Jak mogę przenośnie wywołać funkcję C ++, która na niektórych platformach pobiera znak **, a na innych znak stały **?

91

Na moich komputerach z systemem Linux (i OS X) iconv()funkcja ma następujący prototyp:

size_t iconv (iconv_t, char **inbuf...

podczas gdy we FreeBSD wygląda to tak:

size_t iconv (iconv_t, const char **inbuf...

Chciałbym, aby mój kod C ++ był budowany na obu platformach. W kompilatorach C, przekazanie parametru char**a const char**(lub odwrotnie) zazwyczaj generuje zwykłe ostrzeżenie; jednak w C ++ jest to błąd krytyczny. Więc jeśli zdam char**, nie będzie kompilował się na BSD, a jeśli zdam, const char**nie będzie się kompilował w systemie Linux / OS X. Jak mogę napisać kod, który kompiluje się na obu, bez uciekania się do próby wykrycia platformy?

Jeden (nieudany) pomysł, jaki miałem, polegał na dostarczeniu lokalnego prototypu, który zastąpi wszystkie podane w nagłówku:

void myfunc(void) {
    size_t iconv (iconv_t, char **inbuf);
    iconv(foo, ptr);
}

To się nie udaje, ponieważ iconvwymaga połączenia C i nie możesz wstawić extern "C"funkcji (dlaczego nie?)

Najlepszym działającym pomysłem jest rzutowanie samego wskaźnika funkcji:

typedef void (*func_t)(iconv_t, const char **);
((func_t)(iconv))(foo, ptr);

ale może to maskować inne, poważniejsze błędy.

ridiculous_fish
źródło
31
Piekielne pytanie na pierwsze pytanie na SO. :)
Almo,
24
Zarejestruj błąd we FreeBSD. Implementacja POSIX iconvwymaga, inbufaby element nie był stałą.
dreamlax,
3
Przesyłanie takiej funkcji nie jest przenośne.
Jonathan Grynspan,
2
@dreamlax: przesłanie raportu o błędzie prawdopodobnie nie przyniesie efektu; Aktualna wersja FreeBSD ma podobno już iconvbez const: svnweb.freebsd.org/base/stable/9/include/...
Fred Foo
2
@larsmans: Dobrze wiedzieć! Nigdy nie używałem FreeBSD, ale dobrze wiedzieć, że najnowsza wersja obsługuje najnowszy standard.
dreamlax

Odpowiedzi:

57

Jeśli chcesz po prostu przymknąć oko na niektóre problemy ze stałymi, możesz użyć konwersji, która zaciera różnicę, tj. Sprawia, że ​​char ** i const char ** są interoperacyjne:

template<class T>
class sloppy {}; 

// convert between T** and const T** 
template<class T>
class sloppy<T**>
{
    T** t;
    public: 
    sloppy(T** mt) : t(mt) {}
    sloppy(const T** mt) : t(const_cast<T**>(mt)) {}

    operator T** () const { return t; }
    operator const T** () const { return const_cast<const T**>(t); }
};

Następnie w programie:

iconv(c, sloppy<char**>(&in) ,&inlen, &out,&outlen);

sloppy () przyjmuje a char**lub a const char*i konwertuje je na a char**lub a const char*, niezależnie od tego, czego wymaga drugi parametr iconv.

UPDATE: zmieniono na używanie const_cast i wywołanie niechlujstwa, a nie jako cast.

Nordic Mainframe
źródło
Działa to całkiem dobrze i wydaje się być bezpieczne i proste bez wymagania C ++ 11. Idę z tym! Dzięki!
ridiculous_fish,
2
Jak powiedziałem w mojej odpowiedzi, myślę, że to narusza ścisłe aliasingu w C ++ 03, więc w tym sensie, że nie wymagają c ++ 11. Mogę się jednak mylić, jeśli ktoś chce tego bronić.
Steve Jessop,
1
Nie zachęcaj do rzutowania w stylu C w C ++; chyba że się mylę, możesz sloppy<char**>()bezpośrednio wywołać inicjalizator.
Michał Górny
Oczywiście nadal jest to ta sama operacja, co rzutowanie w stylu C, ale przy użyciu alternatywnej składni C ++. Myślę, że może to zniechęcić czytelników do używania rzutów w stylu C w innych sytuacjach. Na przykład składnia C ++ nie działałaby dla rzutowania, (char**)&inchyba że najpierw utworzysz typedef dla char**.
Steve Jessop,
Dobry hack. Dla kompletności, prawdopodobnie możesz to zrobić albo (a) zawsze przyjmując const char * const *, zakładając, że zmienna nie powinna być zmieniana, lub (b) sparametryzować dowolne dwa typy i sprawić, że będzie stała między nimi.
Jack V.,
33

Możesz rozróżnić dwie deklaracje, sprawdzając podpis zadeklarowanej funkcji. Oto podstawowy przykład szablonów wymaganych do sprawdzenia typu parametru. Można to łatwo uogólnić (lub użyć cech funkcji Boost), ale to wystarczy, aby zademonstrować rozwiązanie konkretnego problemu:

#include <iostream>
#include <stddef.h>
#include <type_traits>

// I've declared this just so the example is portable:
struct iconv_t { };

// use_const<decltype(&iconv)>::value will be 'true' if the function is
// declared as taking a char const**, otherwise ::value will be false.
template <typename>
struct use_const;

template <>
struct use_const<size_t(*)(iconv_t, char**, size_t*, char**, size_t*)>
{
    enum { value = false };
};

template <>
struct use_const<size_t(*)(iconv_t, char const**, size_t*, char**, size_t*)>
{
    enum { value = true };
};

Oto przykład demonstrujący zachowanie:

size_t iconv(iconv_t, char**, size_t*, char**, size_t*);
size_t iconv_const(iconv_t, char const**, size_t*, char**, size_t*);

int main()
{
    using std::cout;
    using std::endl;

    cout << "iconv: "       << use_const<decltype(&iconv)      >::value << endl;
    cout << "iconv_const: " << use_const<decltype(&iconv_const)>::value << endl;
}

Po wykryciu kwalifikacji typu parametru możesz napisać dwie funkcje opakowujące, które wywołują iconv: jedną wywołującą iconvz char const**argumentem i jedną, która wywołujeiconv z char**argumentem.

Ponieważ należy unikać specjalizacji szablonów funkcji, do specjalizacji używamy szablonu klasy. Zauważ, że każdy wywoływacz jest również szablonem funkcji, aby mieć pewność, że utworzona zostanie tylko ta specjalizacja, której używamy. Jeśli kompilator spróbuje wygenerować kod dla złej specjalizacji, otrzymasz błędy.

Następnie zawijamy ich użycie, call_iconvaby wywołanie tego było tak proste, jak iconvbezpośrednie wywołanie . Poniżej znajduje się ogólny wzorzec pokazujący, jak można to zapisać:

template <bool UseConst>
struct iconv_invoker
{
    template <typename T>
    static size_t invoke(T const&, /* arguments */) { /* etc. */ }
};

template <>
struct iconv_invoker<true>
{
    template <typename T>
    static size_t invoke(T const&, /* arguments */) { /* etc. */ }
};

size_t call_iconv(/* arguments */)
{
    return iconv_invoker<
        use_const<decltype(&iconv)>::value
    >::invoke(&iconv, /* arguments */);
}

(Tę drugą logikę można by wyczyścić i uogólnić; starałem się, aby każdy jej element był wyraźny, aby, miejmy nadzieję, wyjaśnić, jak to działa).

James McNellis
źródło
3
Niezła magia. :) Głosowałbym za, ponieważ wygląda na to, że odpowiada na pytanie, ale nie zweryfikowałem, czy to działa, i nie znam wystarczająco ostrego C ++, aby wiedzieć, czy działa, po prostu patrząc na to. :)
Almo,
7
Uwaga: decltypewymaga C ++ 11.
Michał Górny
1
+1 Lol ... więc aby uniknąć #ifdefsprawdzania platformy, otrzymujesz 30 nieparzystych linii kodu :) Jednak miłe podejście (chociaż w ciągu ostatnich kilku dni martwiłem się, patrząc na pytania dotyczące SO, że ludzie, którzy nie naprawdę rozumieją, co robią, zaczęli używać SFINAE jako złotego młotka ... nie w twoim przypadku, ale obawiam się, że kod stanie się bardziej złożony i trudny do utrzymania ...)
David Rodríguez - dribeas
11
@ DavidRodríguez-dribeas: :-) Po prostu kieruję się złotą zasadą współczesnego C ++: jeśli coś nie jest szablonem, zadaj sobie pytanie „dlaczego to nie jest szablon?” następnie zrób z tego szablon.
James McNellis,
1
[Zanim ktokolwiek potraktuje ten ostatni komentarz zbyt poważnie: to żart.
Coś w
11

Możesz użyć następujących:

template <typename T>
size_t iconv (iconv_t i, const T inbuf)
{
   return iconv(i, const_cast<T>(inbuf));
}

void myfunc(void) {
  const char** ptr = // ...
  iconv(foo, ptr);
}

Możesz przekazać const char**i na Linux / OSX przejdzie przez funkcję szablonu, a we FreeBSD przejdzie bezpośrednio do iconv.

Wada: zezwoli na wywołania takie jak, iconv(foo, 2.5)które ustawią kompilator w nieskończonej powtarzalności.

Krizz
źródło
2
Ładny! Myślę, że to rozwiązanie ma potencjał: podoba mi się użycie rozdzielczości przeciążenia, aby wybrać szablon tylko wtedy, gdy funkcja nie jest dokładnie dopasowana. Aby jednak zadziałało, const_castnależałoby przenieść go do elementu, add_or_remove_constktóry zagłębia się w, T**aby wykryć, czy Tjest, consti odpowiednio dodać lub usunąć kwalifikację. Byłoby to (o wiele) prostsze niż rozwiązanie, które pokazałem. Przy odrobinie pracy może być również możliwe sprawienie, aby to rozwiązanie działało bez const_cast(tj. Używając zmiennej lokalnej w twoim iconv).
James McNellis,
Czy coś przegapiłem? W przypadku, gdy wartość rzeczywista iconvnie jest stała , nie jest Twydedukowana jako const char**, co oznacza, że ​​parametr inbufma typ const T, czyli jest const char **const, a wywołanie iconvw szablonie po prostu wywołuje siebie? Jak jednak mówi James, z odpowiednią modyfikacją typu Tta sztuczka jest podstawą czegoś, co działa.
Steve Jessop,
Niesamowite, sprytne rozwiązanie. +1!
Linuxios
7
#ifdef __linux__
... // linux code goes here.
#elif __FreeBSD__
... // FreeBSD code goes here.
#endif

Tutaj masz identyfikatory wszystkich systemów operacyjnych. Dla mnie nie ma sensu próbować robić czegoś, co zależy od systemu operacyjnego, bez sprawdzania tego systemu. To jak kupowanie zielonych spodni, ale bez patrzenia na nie.

Krew
źródło
14
Ale pytający wyraźnie mówi without resorting to trying to detect the platform...
Frédéric Hamidi
1
@Linuxios: aż Linux dostawców lub jabłko decydują oni nie chcą podążać za standardem POSIX . Ten rodzaj kodowania jest niezwykle trudny do utrzymania.
Fred Foo,
2
@larsmans: Linux i Mac OS X zgodne ze standardem . Twój link pochodzi z 1997 r. To FreeBSD jest z tyłu.
dreamlax,
3
@Linuxios: Nie, nie jest [lepiej]. Jeśli naprawdę chcesz sprawdzić platformę, użyj autoconf lub podobnego narzędzia. Sprawdź rzeczywisty prototyp, zamiast robić założenia, które w pewnym momencie zawiodą i u użytkownika.
Michał Górny
2
@ MichałGórny: Słuszna uwaga. Szczerze mówiąc, powinienem po prostu wyjść z tego pytania. Wydaje się, że nie jestem w stanie nic do tego wnieść.
Linuxios,
1

Wskazałeś, że używanie własnej funkcji opakowującej jest dopuszczalne. Wydaje się, że chcesz żyć z ostrzeżeniami.

Więc zamiast pisać swój wrapper w C ++, napisz go w C, gdzie otrzymasz ostrzeżenie tylko w niektórych systemach:

// my_iconv.h

#if __cpluscplus
extern "C" {
#endif

size_t my_iconv( iconv_t cd, char **restrict inbuf, ?* etc... */);


#if __cpluscplus
}
#endif



// my_iconv.c
#include <iconv.h>
#include "my_iconv.h"

size_t my_iconv( iconv_t cd, char **inbuf, ?* etc... */)
{
    return iconv( cd, 
                inbuf /* will generate a warning on FreeBSD */,
                /* etc... */
                );
}
Michael Burr
źródło
1

Co powiesz na

static void Test(char **)
{
}

int main(void)
{
    const char *t="foo";
    Test(const_cast<char**>(&t));
    return 0;
}

EDYCJA: oczywiście „bez wykrywania platformy” jest trochę problemem. Ups :-(

EDYCJA 2: ok, może ulepszona wersja?

static void Test(char **)
{
}

struct Foo
{
    const char **t;

    operator char**() { return const_cast<char**>(t); }
    operator const char**() { return t; }

    Foo(const char* s) : t(&s) { }
};

int main(void)
{
    Test(Foo("foo"));
    return 0;
}
Christian Stieber
źródło
Problem polega na tym, że na innej platformie nie da się skompilować (tzn. Jeśli funkcja przyjmie a const char**to się nie powiedzie)
David Rodríguez - dribeas
1

Co powiesz na:

#include <cstddef>
using std::size_t;

// test harness, these definitions aren't part of the solution
#ifdef CONST_ICONV
    // other parameters removed for tediousness
    size_t iconv(const char **inbuf) { return 0; }
#else
    // other parameters removed for tediousness
    size_t iconv(char **inbuf) { return 0; }
#endif

// solution
template <typename T>
size_t myconv_helper(size_t (*system_iconv)(T **), char **inbuf) {
    return system_iconv((T**)inbuf); // sledgehammer cast
}

size_t myconv(char **inbuf) {
    return myconv_helper(iconv, inbuf);
}

// usage
int main() {
    char *foo = 0;
    myconv(&foo);
}

Myślę, że narusza to ścisłe aliasy w C ++ 03, ale nie w C ++ 11, ponieważ w C ++ 11 const char**i char**są to tak zwane „podobne typy”. Nie zamierzasz uniknąć tego naruszenia ścisłego aliasingu, poza utworzeniem a const char*, ustawieniem go na równe *foo, wywołaniem iconvze wskaźnikiem do tymczasowego, a następnie skopiowaniem wyniku z powrotem *foopo const_cast:

template <typename T>
size_t myconv_helper(size_t (*system_iconv)(T **), char **inbuf) {
    T *tmpbuf;
    tmpbuf = *inbuf;
    size_t result = system_iconv(&tmpbuf);
    *inbuf = const_cast<char*>(tmpbuf);
    return result;
}

Jest to bezpieczne od punktu widzenia stałej poprawności, ponieważ wszystko iconvma związek zinbuf to inkrementacja przechowywanego w nim wskaźnika. Więc „odrzucamy stałą” ze wskaźnika wywodzącego się ze wskaźnika, który nie był stałą, kiedy pierwszy raz go zobaczyliśmy.

Moglibyśmy również napisać przeciążenie myconvi, myconv_helperktóre zabierają const char **inbufi komplikują rzeczy w innym kierunku, tak aby dzwoniący miał wybór, czy przekazać a const char**lub a char**. Co prawdopodobnie iconvpowinno było dać wywołującemu na pierwszym miejscu w C ++, ale oczywiście interfejs jest po prostu kopiowany z C, gdzie nie ma przeciążenia funkcji.

Steve Jessop
źródło
Kod „super-pedanterii” jest niepotrzebny. W GCC4.7 z aktualnym standardowym biblioteką c ++, potrzebujesz tego do kompilacji.
Konrad Rudolph,
1

Aktualizacja: teraz widzę, że da się to obsłużyć w C ++ bez narzędzi autotools, jednak rozwiązanie autoconf zostawiam osobom, które go szukają.

To, czego szukasz, iconv.m4jest instalowane przez pakiet gettext.

AFAICS to po prostu:

AM_ICONV

w configure.ac i powinien wykryć poprawny prototyp.

Następnie w kodzie, którego używasz:

#ifdef ICONV_CONST
// const char**
#else
// char**
#endif
Michał Górny
źródło
użyj do tego specjalizacji szablonowej. patrz wyżej.
Alex
1
Dzięki! Używam już narzędzi automatycznych i wydaje się, że jest to standardowy sposób obejścia tego problemu, więc powinien być doskonały! Niestety nie udało mi się uzyskać autoconf, aby znaleźć plik iconv.m4 (i wygląda na to, że nie istnieje na OS X, który ma starożytną wersję autotools), więc nie mogłem go uruchomić przenośnie . Wyszukiwanie w Google pokazuje, że wiele osób ma problemy z tym makrem. Och, automatyczne narzędzia!
ridiculous_fish
Myślę, że moja odpowiedź jest brzydka, ale nie ryzykowna. Mimo to, jeśli już używasz autoconf i jeśli niezbędna konfiguracja istnieje na platformach, na których Ci zależy, nie ma prawdziwego powodu, aby tego nie używać ...
Steve Jessop
W moim systemie ten plik .m4 jest instalowany przez gettextpakiet. Ponadto dość często zdarza się, że pakiety zawierają używane makra w m4/katalogu i mają ACLOCAL_AMFLAGS = -I m4w Makefile.am. Myślę, że autopoint nawet domyślnie kopiuje go do tego katalogu.
Michał Górny
0

Spóźniłem się na tę imprezę, ale oto moje rozwiązanie:

// This is here because some compilers (Sun CC) think that there is a
// difference if the typedefs are not in an extern "C" block.
extern "C"
{
//! SUSv3 iconv() type.
typedef size_t (& iconv_func_type_1) (iconv_t cd, char * * inbuf,
    size_t * inbytesleft, char * * outbuf, size_t * outbytesleft); 


//! GNU iconv() type.
typedef size_t (& iconv_func_type_2) (iconv_t cd, const char * * inbuf,
    size_t * inbytesleft, char * * outbuf, size_t * outbytesleft);
} // extern "C"

//...

size_t
call_iconv (iconv_func_type_1 iconv_func, char * * inbuf,
    size_t * inbytesleft, char * * outbuf, size_t * outbytesleft)
{
    return iconv_func (handle, inbuf, inbytesleft, outbuf, outbytesleft);
}

size_t
call_iconv (iconv_func_type_2 iconv_func, char * * inbuf,
    size_t * inbytesleft, char * * outbuf, size_t * outbytesleft)
{
    return iconv_func (handle, const_cast<const char * *>(inbuf),
        inbytesleft, outbuf, outbytesleft);
}

size_t
do_iconv (char * * inbuf, size_t * inbytesleft, char * * outbuf,
    size_t * outbytesleft)
{
    return call_iconv (iconv, inbuf, inbytesleft, outbuf, outbytesleft);
}
wilx
źródło