Comment puis-je appeler de manière portative une fonction C ++ qui prend un char ** sur certaines plates-formes et un const char ** sur d'autres?

91

Sur mes machines Linux (et OS X), la iconv()fonction a ce prototype:

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

tandis que sur FreeBSD, cela ressemble à ceci:

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

J'aimerais que mon code C ++ soit basé sur les deux plates-formes. Avec les compilateurs C, passer un char**pour un const char**paramètre (ou vice versa) émet généralement un simple avertissement; cependant en C ++ c'est une erreur fatale. Donc si je passe a char**, il ne se compilera pas sur BSD, et si je passe a, const char**il ne se compilera pas sous Linux / OS X. Comment puis-je écrire du code qui compile sur les deux, sans avoir recours à la détection de la plate-forme?

Une idée (ratée) que j'ai eue était de fournir un prototype local qui remplace tous ceux fournis par l'en-tête:

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

Cela échoue car iconvnécessite une liaison C et vous ne pouvez pas mettre extern "C"dans une fonction (pourquoi pas?)

La meilleure idée de travail que j'ai proposée est de lancer le pointeur de fonction lui-même:

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

mais cela a le potentiel de masquer d'autres erreurs plus graves.

poisson_ ridicule
la source
31
Enfer d'une question pour votre première sur SO. :)
Almo
24
Enregistrez un bogue sur FreeBSD. L'implémentation POSIX de iconvnécessite que le inbufsoit non-const.
dreamlax
3
Diffuser la fonction comme ça n'est pas portable.
Jonathan Grynspan
2
@dreamlax: il est peu probable que la soumission d'un rapport de bogue ait un effet; la version actuelle de FreeBSD a apparemment déjà iconvsans le const: svnweb.freebsd.org/base/stable/9/include/…
Fred Foo
2
@larsmans: C'est bon à savoir! Je n'ai jamais utilisé FreeBSD mais il est bon de savoir que la dernière version prend en charge la dernière norme.
dreamlax

Réponses:

57

Si vous voulez simplement fermer les yeux sur certains problèmes de const, vous pouvez utiliser une conversion qui brouille la distinction, c'est-à-dire rend les char ** et const char ** interopérables:

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); }
};

Puis plus tard dans le programme:

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

bâclée () prend un char**ou const char*et le convertit en un char**ou const char*, quel que soit le deuxième paramètre de iconv demande.

UPDATE: changé pour utiliser const_cast et appeler sloppy not a as cast.

Mainframe nordique
la source
Cela fonctionne assez bien et semble être sûr et simple sans nécessiter C ++ 11. Je vais avec! Merci!
ridiculous_fish
2
Comme je l' ai dit dans ma réponse, je pense que cela constitue une violation aliasing stricte en C ++ 03, donc en ce sens , il ne nécessite C ++ 11. Je me trompe peut-être si quelqu'un veut défendre cela.
Steve Jessop
1
Veuillez ne pas encourager les transtypages de style C en C ++; sauf si je me trompe, vous pouvez appeler l' sloppy<char**>()initialiseur directement là-bas.
Michał Górny
C'est toujours la même opération qu'un cast de style C bien sûr, mais en utilisant la syntaxe C ++ alternative. Je suppose que cela pourrait décourager les lecteurs d'utiliser des moulages de style C dans d'autres situations. Par exemple, la syntaxe C ++ ne fonctionnerait pas pour le cast, (char**)&insauf si vous créez d'abord un typedef pour char**.
Steve Jessop du
Bon hack. Par souci d'exhaustivité, vous pourriez probablement faire ceci soit (a) en prenant toujours un const char * const *, en supposant que la variable n'est pas censée être modifiée ou (b) en paramétrant par deux types quelconques et en la convertissant const entre eux.
Jack V.
33

Vous pouvez lever l'ambiguïté entre les deux déclarations en inspectant la signature de la fonction déclarée. Voici un exemple de base des modèles requis pour inspecter le type de paramètre. Cela pourrait facilement être généralisé (ou vous pouvez utiliser les traits de fonction de Boost), mais cela suffit pour démontrer une solution à votre problème spécifique:

#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 };
};

Voici un exemple qui illustre le comportement:

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;
}

Une fois que vous pouvez détecter la qualification du type de paramètre, vous pouvez écrire deux fonctions wrapper qui appellent iconv: une qui appelle iconvavec un char const**argument et une qui appelle iconvavec un char**argument.

Étant donné que la spécialisation du modèle de fonction doit être évitée, nous utilisons un modèle de classe pour effectuer la spécialisation. Notez que nous faisons également de chacun des invocateurs un modèle de fonction, pour nous assurer que seule la spécialisation que nous utilisons est instanciée. Si le compilateur essaie de générer du code pour la mauvaise spécialisation, vous obtiendrez des erreurs.

Nous enveloppons ensuite l'utilisation de ceux-ci avec un call_iconvpour rendre cet appel aussi simple que d'appeler iconvdirectement. Voici un schéma général montrant comment cela peut être écrit:

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 */);
}

(Cette dernière logique pourrait être nettoyée et généralisée; j'ai essayé de rendre chaque élément explicite pour, espérons-le, clarifier son fonctionnement.)

James McNellis
la source
3
Belle magie là-bas. :) Je voterais pour parce qu'il semble que cela répond à la question, mais je n'ai pas vérifié que cela fonctionne, et je ne connais pas assez le C ++ hardcore pour savoir si c'est le cas simplement en le regardant. :)
Almo
7
À noter: decltypenécessite C ++ 11.
Michał Górny
1
+1 Lol ... donc pour éviter de #ifdefvérifier la plate-forme, vous vous retrouvez avec 30 lignes de code impaires :) Bonne approche cependant (même si je me suis inquiété ces derniers jours en regardant des questions sur SO que les gens qui ne le font pas vraiment comprendre ce qu'ils font ont commencé à utiliser SFINAE comme un marteau d'or ... ce n'est pas votre cas, mais je crains que le code ne devienne plus complexe et difficile à maintenir ...)
David Rodríguez - dribeas
11
@ DavidRodríguez-dribeas: :-) Je suis juste la règle d'or du C ++ moderne: si quelque chose n'est pas un modèle, demandez-vous, "pourquoi n'est-ce pas un modèle?" puis faites-en un modèle.
James McNellis
1
[Avant que quiconque prenne ce dernier commentaire trop au sérieux: c'est une blague. Sort of ...]
James McNellis
11

Vous pouvez utiliser les éléments suivants:

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);
}

Vous pouvez passer const char**et sous Linux / OSX, il passera par la fonction de modèle et sur FreeBSD, il ira directement iconv.

Inconvénient: cela permettra des appels comme ceux iconv(foo, 2.5)qui mettront le compilateur en récurrence infinie.

Krizz
la source
2
Agréable! Je pense que cette solution a du potentiel: j'aime l'utilisation de la résolution de surcharge pour sélectionner le modèle uniquement lorsque la fonction n'est pas une correspondance exacte. Pour fonctionner, cependant, le const_castdevrait être déplacé dans un add_or_remove_constqui creuse dans le T**pour détecter si Test constet ajouter ou supprimer la qualification le cas échéant. Ce serait encore (beaucoup) plus simple que la solution que j'ai démontrée. Avec un peu de travail, il pourrait également être possible de faire fonctionner cette solution sans le const_cast(c'est-à-dire en utilisant une variable locale dans votre iconv).
James McNellis
Ai-je manqué quelque chose? Dans le cas où le réel iconvest non-const, n'est pas Tdéduit comme const char**, ce qui signifie que le paramètre inbufa le type const T, qui est const char **const, et l'appel à iconvdans le modèle s'appelle simplement lui-même? Comme James le dit cependant, avec une modification appropriée du type, Tcette astuce est la base de quelque chose qui fonctionne.
Steve Jessop
Solution géniale et intelligente. +1!
Linuxios
7
#ifdef __linux__
... // linux code goes here.
#elif __FreeBSD__
... // FreeBSD code goes here.
#endif

Ici, vous avez les identifiants de tous les systèmes d'exploitation. Pour moi, cela ne sert à rien d'essayer de faire quelque chose qui dépend du système d'exploitation sans vérifier ce système. C'est comme acheter un pantalon vert mais sans les regarder.

Du sang
la source
13
Mais le questionneur dit explicitement without resorting to trying to detect the platform...
Frédéric Hamidi
1
@Linuxios: jusqu'à ce que les vendeurs de Linux ou Apple décident qu'ils ne veulent suivre la norme POSIX . Ce type de codage est notoirement difficile à maintenir.
Fred Foo
2
@larsmans: Linux et Mac OS X font suivre la norme . Votre lien date de 1997. C'est FreeBSD qui est derrière.
dreamlax
3
@Linuxios: Non, ce n'est pas [mieux]. Si vous voulez vraiment faire des vérifications de plate-forme, utilisez autoconf ou un outil similaire. Vérifiez le prototype réel plutôt que de faire des hypothèses qui échoueront à un moment donné et qui échoueront pour l'utilisateur.
Michał Górny
2
@ MichałGórny: Bon point. Franchement, je devrais simplement sortir de cette question. Je ne semble pas pouvoir y apporter quoi que ce soit.
Linuxios
1

Vous avez indiqué que l'utilisation de votre propre fonction wrapper est acceptable. Vous semblez également vouloir vivre avec des avertissements.

Donc, au lieu d'écrire votre wrapper en C ++, écrivez-le en C, où vous n'obtiendrez un avertissement que sur certains systèmes:

// 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
la source
1

Que diriez-vous

static void Test(char **)
{
}

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

EDIT: bien sûr, le "sans détecter la plate-forme" est un peu un problème. Oups :-(

EDIT 2: ok, version améliorée, peut-être?

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
la source
Le problème avec cela est que sur l'autre plate-forme, il ne se compilera pas (c'est-à-dire que si la fonction prend un, const char**cela échouera)
David Rodríguez - dribeas
1

Qu'en est-il de:

#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);
}

Je pense que cela viole l'aliasing strict en C ++ 03, mais pas en C ++ 11 car en C ++ 11 const char**et char**sont des soi-disant «types similaires». Vous n'allez pas éviter cette violation de l'alias strict autrement qu'en créant un const char*, définissez-le égal à *foo, appelez iconvavec un pointeur vers le temporaire, puis recopiez le résultat *fooaprès un 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;
}

Ceci est à l'abri du POV de const-correctness, car il iconvne inbufs'agit que d'incrémenter le pointeur qui y est stocké. Donc, nous «rejetons const» à partir d'un pointeur dérivé d'un pointeur qui n'était pas const lorsque nous l'avons vu pour la première fois.

Nous pourrions également écrire une surcharge de myconvet myconv_helperqui prennent const char **inbufet gâchent les choses dans l'autre sens, de sorte que l'appelant ait le choix de passer en a const char**ou en a char**. Ce qui iconvaurait sans doute dû être donné à l'appelant en premier lieu en C ++, mais bien sûr, l'interface est simplement copiée à partir de C où il n'y a pas de surcharge de fonction.

Steve Jessop
la source
Le code "super-pédanterie" est inutile. Sur GCC4.7 avec un stdlibc ++ actuel, vous en avez besoin pour compiler.
Konrad Rudolph
1

Mise à jour: maintenant je vois qu'il est possible de le gérer en C ++ sans autotools, mais je laisse la solution autoconf aux personnes qui la recherchent.

Ce que vous recherchez est celui iconv.m4qui est installé par le package gettext.

AFAICS c'est juste:

AM_ICONV

dans configure.ac, et il devrait détecter le bon prototype.

Ensuite, dans le code que vous utilisez:

#ifdef ICONV_CONST
// const char**
#else
// char**
#endif
Michał Górny
la source
utilisez la spécialisation des modèles pour cela. voir au dessus.
Alex
1
Merci! J'utilise déjà des outils automatiques, et cela semble être le moyen standard de contourner le problème, donc cela devrait être parfait! Malheureusement, je n'ai pas pu obtenir autoconf pour trouver le fichier iconv.m4 (et il ne semble pas exister sur OS X, qui a une ancienne version d'autotools), donc je n'ai pas pu le faire fonctionner de manière portable . Googler montre que beaucoup de gens ont des problèmes avec cette macro. Oh, les outils automatiques!
ridiculous_fish
Je pense que j'ai un hack moche mais non risqué dans ma réponse. Pourtant, si vous utilisez déjà autoconf, et si la configuration nécessaire existe sur les plates-formes qui vous intéressent, il n'y a aucune vraie raison de ne pas l'utiliser ...
Steve Jessop
Sur mon système, ce fichier .m4 est installé par gettextpackage. De plus, il est assez courant pour les packages d'inclure des macros utilisées dans le m4/répertoire et d'avoir des ACLOCAL_AMFLAGS = -I m4fichiers Makefile.am. Je pense qu'autopoint le copie même dans ce répertoire par défaut.
Michał Górny
0

Je suis en retard à cette soirée mais quand même, voici ma solution:

// 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
la source