Como posso chamar portavelmente uma função C ++ que usa um char ** em algumas plataformas e um const char ** em outras?

91

Em minhas máquinas Linux (e OS X), a iconv()função tem este protótipo:

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

enquanto no FreeBSD é assim:

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

Eu gostaria que meu código C ++ fosse construído em ambas as plataformas. Com compiladores C, passar um char**para um const char**parâmetro (ou vice-versa) normalmente emite um mero aviso; entretanto, em C ++, é um erro fatal. Então, se eu passar um char**, ele não compilará no BSD, e se eu passar um, const char**ele não compilará no Linux / OS X. Como posso escrever código que compila em ambos, sem recorrer a tentar detectar a plataforma?

Uma ideia (falhada) que tive foi fornecer um protótipo local que substitui qualquer fornecido pelo cabeçalho:

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

Isso falha porque iconvprecisa de ligação C e você não pode colocar extern "C"dentro de uma função (por que não?)

A melhor ideia de trabalho que tive é lançar o próprio ponteiro de função:

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

mas isso tem o potencial de mascarar outros erros mais sérios.

ridiculous_fish
fonte
31
Inferno de uma pergunta para seu primeiro no SO. :)
Almo
24
Registre um bug no FreeBSD. A implementação POSIX de iconvrequer que o inbufseja não constante.
dreamlax
3
Transmitir a função dessa forma não é portátil.
Jonathan Grynspan
2
@dreamlax: o envio de um relatório de bug provavelmente não terá efeito; a versão atual do FreeBSD aparentemente já iconvsem o const: svnweb.freebsd.org/base/stable/9/include/…
Fred Foo
2
@larsmans: É bom saber! Nunca usei o FreeBSD, mas é bom saber que a versão mais recente suporta o padrão mais recente.
dreamlax

Respostas:

57

Se o que você quer é apenas fechar os olhos para alguns problemas const, você pode usar uma conversão que confunde a distinção, ou seja, torna char ** e const char ** interoperáveis:

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

Em seguida, mais tarde no programa:

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

sloppy () pega a char**ou a const char*e converte para a char**ou a const char*, qualquer que seja o segundo parâmetro de iconv exigir.

ATUALIZAÇÃO: alterado para usar const_cast e chamar sloppy not a como cast.

Nordic Mainframe
fonte
Isso funciona muito bem e parece ser seguro e direto, sem exigir C ++ 11. Eu estou indo com isso! Obrigado!
ridiculous_fish
2
Como eu disse na minha resposta, eu acho que isso viola estrita aliasing em C ++ 03, então, nesse sentido, não requer C ++ 11. Posso estar errado, porém, se alguém quiser defender isso.
Steve Jessop
1
Não incentive conversões de estilo C em C ++; a menos que eu esteja errado, você pode chamar o sloppy<char**>()inicializador diretamente aqui.
Michał Górny
É claro que ainda é a mesma operação que um elenco de estilo C, mas usando a sintaxe C ++ alternativa. Eu acho que isso pode desencorajar os leitores de usar elencos no estilo C em outras situações. Por exemplo, a sintaxe C ++ não funcionaria para o elenco a (char**)&inmenos que você primeiro fizesse um typedef para char**.
Steve Jessop
Good hack. Para completar, você provavelmente poderia fazer isso (a) sempre tomando um const char * const *, assumindo que a variável não deve ser alterada ou (b) parametrizando por quaisquer dois tipos e tornando-o const cast entre eles.
Jack V.
33

Você pode eliminar a ambigüidade entre as duas declarações inspecionando a assinatura da função declarada. Aqui está um exemplo básico dos modelos necessários para inspecionar o tipo de parâmetro. Isso poderia ser facilmente generalizado (ou você poderia usar os traços da função de Boost), mas isso é suficiente para demonstrar uma solução para seu problema específico:

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

Aqui está um exemplo que demonstra o comportamento:

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

Depois de detectar a qualificação do tipo de parâmetro, você pode escrever duas funções de wrapper que chamam iconv: uma que chama iconvcom um char const**argumento e outra que chama iconvcom um char**argumento.

Como a especialização do template de função deve ser evitada, usamos um template de classe para fazer a especialização. Observe que também tornamos cada um dos invocadores um modelo de função, para garantir que apenas a especialização que usamos seja instanciada. Se o compilador tentar gerar código para a especialização errada, você obterá erros.

Em seguida, envolvemos o uso deles com um call_iconvpara tornar a chamada tão simples quanto chamar iconvdiretamente. A seguir está um padrão geral que mostra como isso pode ser escrito:

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

(Esta última lógica poderia ser limpa e generalizada; tentei tornar cada parte dela explícita para, espero, deixar mais claro como funciona.)

James McNellis
fonte
3
Bela mágica aí. :) Eu votaria positivamente porque parece que responde à pergunta, mas não verifiquei se funciona e não conheço C ++ hardcore o suficiente para saber se funciona apenas olhando para ele. :)
Almo
7
Como observação: decltyperequer C ++ 11.
Michał Górny
1
+1 Lol ... então para evitar uma #ifdefverificação para a plataforma você acaba com 30 linhas de código estranhas :) Boa abordagem, embora (embora eu tenha me preocupado nos últimos dias olhando para perguntas sobre SO que as pessoas que não realmente entendo o que eles estão fazendo, comecei a usar SFINAE como um martelo dourado ... não é o seu caso, mas temo que o código se tornará mais complexo e difícil de manter ...)
David Rodríguez - dribeas
11
@ DavidRodríguez-dribeas: :-) Eu apenas sigo a regra de ouro do C ++ moderno: se algo não é um modelo, pergunte-se, "por que isso não é um modelo?" em seguida, torne-o um modelo.
James McNellis
1
[Antes que alguém leve esse último comentário muito a sério: é uma piada. Mais ou menos ...]
James McNellis
11

Você pode usar o seguinte:

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

Você pode passar const char**e no Linux / OSX irá passar pela função de template e no FreeBSD irá diretamente para iconv.

Desvantagem: permitirá chamadas como as iconv(foo, 2.5)que colocarão o compilador em recorrência infinita.

Krizz
fonte
2
Agradável! Acho que essa solução tem potencial: gosto do uso da resolução de sobrecarga para selecionar o modelo apenas quando a função não é uma correspondência exata. Para funcionar, no entanto, o const_castprecisaria ser movido para um add_or_remove_constque analisa o T**para detectar se Té conste adicionar ou remover a qualificação conforme apropriado. Isso ainda seria (muito) mais simples do que a solução que demonstrei. Com um pouco de trabalho, também pode ser possível fazer essa solução funcionar sem o const_cast(ou seja, usando uma variável local no seu iconv).
James McNellis
Eu perdi alguma coisa? No caso em que o real iconvé não const, não é Tdeduzido como const char**, o que significa que o parâmetro inbuftem tipo const T, que é const char **const, e a chamada de iconvno modelo apenas chama a si mesma? Porém, como James disse, com uma modificação adequada no tipo, Tesse truque é a base de algo que funciona.
Steve Jessop
Solução incrível e inteligente. +1!
Linuxios
7
#ifdef __linux__
... // linux code goes here.
#elif __FreeBSD__
... // FreeBSD code goes here.
#endif

Aqui você tem ids de todos os sistemas operacionais. Para mim não adianta tentar fazer algo que depende do sistema operacional sem verificar este sistema. É como comprar calças verdes mas sem olhar para elas.

Sangue
fonte
13
Mas o questionador diz explicitamente without resorting to trying to detect the platform...
Frédéric Hamidi
1
@Linuxios: até Linux fornecedores ou Apple decidem que não querem seguir o padrão POSIX . Esse tipo de codificação é notoriamente difícil de manter.
Fred Foo
2
@larsmans: Linux e Mac OS X fazer seguir o padrão . Seu link é de 1997. É o FreeBSD que está por trás.
dreamlax
3
@Linuxios: Não, não é [melhor]. Se você realmente deseja fazer verificações de plataforma, use o autoconf ou uma ferramenta semelhante. Verifique o protótipo real ao invés de fazer suposições que irão falhar em algum ponto e falhará no usuário.
Michał Górny
2
@ MichałGórny: Bom ponto. Francamente, eu deveria simplesmente sair desta questão. Não pareço ser capaz de contribuir com nada para isso.
Linuxios
1

Você indicou que usar sua própria função de wrapper é aceitável. Você também parece estar disposto a conviver com os avisos.

Portanto, em vez de escrever seu wrapper em C ++, escreva-o em C, onde você receberá apenas um aviso em alguns sistemas:

// 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
fonte
1

E se

static void Test(char **)
{
}

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

EDIT: claro, o "sem detectar a plataforma" é um pouco problemático. Ops :-(

EDIT 2: ok, versão melhorada, talvez?

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
fonte
O problema com isso é que na outra plataforma ele não compilará (ou seja, se a função const char**
receber
1

A respeito:

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

Eu acho que isso viola o aliasing estrito em C ++ 03, mas não em C ++ 11 porque em C ++ 11 const char**e char**são chamados de "tipos semelhantes". Você não vai evitar essa violação de aliasing estrito, exceto criando um const char*, defina-o igual a *foo, chame iconvcom um ponteiro para o temporário e copie o resultado de volta para *foodepois de 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;
}

Isso está protegido do POV de const-correção, porque tudo o que iconvfaz inbufé incrementar o ponteiro armazenado nele. Portanto, estamos "descartando const" de um ponteiro derivado de um ponteiro que não era const quando o vimos pela primeira vez.

Também poderíamos escrever uma sobrecarga de myconve myconv_helperque leva const char **inbufe bagunça as coisas na outra direção, de modo que o chamador tenha a opção de passar por a const char**ou a char**. Que indiscutivelmente iconvdeveria ter dado ao chamador em primeiro lugar em C ++, mas é claro que a interface é apenas copiada de C onde não há sobrecarga de função.

Steve Jessop
fonte
O código do "super-pedantismo" é desnecessário. Em GCC4.7 com um stdlibc ++ atual, você precisa disso para compilar.
Konrad Rudolph
1

Atualização: agora vejo que é possível lidar com isso em C ++ sem autotools, mas estou deixando a solução autoconf para quem procura por ela.

O que você está procurando é o iconv.m4que é instalado pelo pacote gettext.

AFAICS é apenas:

AM_ICONV

em configure.ac, e deve detectar o protótipo correto.

Então, no código que você usa:

#ifdef ICONV_CONST
// const char**
#else
// char**
#endif
Michał Górny
fonte
use a especialização de modelo para isso. Veja acima.
Alex
1
Obrigado! Eu já uso o autotools e esta parece ser a maneira padrão de contornar o problema, então deve ser perfeita! Infelizmente não consegui fazer com que o autoconf encontrasse o arquivo iconv.m4 (e ele parece não existir no OS X, que tem uma versão antiga do autotools), então não consegui fazê-lo funcionar de forma portátil . Procurar no Google mostra que muitas pessoas têm problemas com essa macro. Oh, ferramentas automáticas!
ridiculous_fish
Acho que tenho um hack feio, mas não arriscado, em minha resposta. Ainda assim, se você já estiver usando o autoconf, e se a configuração necessária existir nas plataformas de seu interesse, não há razão real para não usar isso ...
Steve Jessop
No meu sistema, esse arquivo .m4 é instalado por gettextpacote. Além disso, é bastante comum que os pacotes incluam macros usadas no m4/diretório e tenham ACLOCAL_AMFLAGS = -I m4em Makefile.am. Acho que o autopoint até copia para esse diretório por padrão.
Michał Górny
0

Estou atrasado para esta festa, mas mesmo assim, aqui está a minha solução:

// 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
fonte