Как я могу переносимо вызвать функцию C ++, которая принимает char ** на некоторых платформах и const char ** на других?

91

На моих машинах с Linux (и OS X) iconv()функция имеет этот прототип:

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

а на FreeBSD это выглядит так:

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

Я бы хотел, чтобы мой код C ++ работал на обеих платформах. В компиляторах C передача a char**для const char**параметра (или наоборот) обычно вызывает простое предупреждение; однако в C ++ это фатальная ошибка. Так что, если я передаю char**, он не будет компилироваться в BSD, а если я передам, const char**он не будет компилироваться в Linux / OS X. Как я могу написать код, который компилируется на обоих, не прибегая к попыткам обнаружения платформы?

Одна (неудачная) идея, которая у меня была, заключалась в том, чтобы предоставить локальный прототип, который переопределяет все, что указано в заголовке:

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

Это не удается, потому что iconvтребуется связь с C, и вы не можете вставить extern "C"функцию (почему бы и нет?)

Лучшая рабочая идея, которую я придумал, - привести сам указатель функции:

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

но это может замаскировать другие, более серьезные ошибки.

смешная_рыба
источник
31
Адский вопрос для вашего первого на SO. :)
Almo
24
Зарегистрируйте ошибку в FreeBSD. Реализация POSIX iconvтребует, inbufчтобы объект не был константным.
dreamlax
3
Приведение такой функции непереносимо.
Jonathan Grynspan
2
@dreamlax: отправка отчета об ошибке вряд ли даст эффект; текущая версия FreeBSD , видимо , уже iconvбез const: svnweb.freebsd.org/base/stable/9/include/...
Fred Foo
2
@larsmans: Приятно знать! Я никогда не использовал FreeBSD, но хорошо знать, что последняя версия поддерживает последний стандарт.
dreamlax

Ответы:

57

Если вы хотите просто закрыть глаза на некоторые проблемы с константой, вы можете использовать преобразование, которое стирает различие, то есть делает char ** и const char ** совместимыми:

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

Потом в программе:

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

sloppy () принимает a char**или a const char*и преобразует его в a char**или a const char*, независимо от того, что требует второй параметр iconv.

ОБНОВЛЕНИЕ: изменено использование const_cast и вызов sloppy not a as cast.

Скандинавский мэйнфрейм
источник
Это работает довольно хорошо и кажется безопасным и простым, не требуя C ++ 11. Я пойду с этим! Благодарность!
ridiculous_fish
2
Как я уже говорил в моем ответе, я думаю , что это нарушает строгую ступенчатость в C ++ 03, так что в этом смысле это действительно требует C ++ 11. Я могу ошибаться, если кто-то хочет это защитить.
Стив Джессоп,
1
Пожалуйста, не поощряйте приведение типов в стиле C в C ++; если я не ошибаюсь, вы можете вызвать sloppy<char**>()инициализатор прямо там.
Michał Górny
Это, конечно же, та же операция, что и приведение в стиле C, но с использованием альтернативного синтаксиса C ++. Думаю, это может отговорить читателей от использования приведений в стиле C в других ситуациях. Например, синтаксис C ++ не будет работать для приведения, (char**)&inесли вы сначала не создадите typedef для char**.
Steve Jessop
Хороший хак. Для полноты картины вы, вероятно, могли бы сделать это либо (а) всегда с использованием const char * const *, предполагая, что переменная не должна изменяться, либо (б) параметризацией любыми двумя типами и преобразованием константы между ними.
Джек В.
33

Вы можете устранить неоднозначность между двумя объявлениями, проверив подпись объявленной функции. Вот базовый пример шаблонов, необходимых для проверки типа параметра. Это можно легко обобщить (или вы можете использовать свойства функции Boost), но этого достаточно, чтобы продемонстрировать решение вашей конкретной проблемы:

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

Вот пример, демонстрирующий поведение:

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

Как только вы сможете определить квалификацию типа параметра, вы можете написать две функции-оболочки, которые вызывают iconv: одну, которая вызывает iconvсchar const** аргументом, а другую - iconvс char**аргументом.

Поскольку следует избегать специализации шаблонов функций, мы используем шаблон класса для выполнения специализации. Обратите внимание, что мы также делаем каждого из вызывающих шаблонов функцией, чтобы гарантировать, что создается только та специализация, которую мы используем. Если компилятор попытается сгенерировать код для неправильной специализации, вы получите ошибки.

Затем мы оборачиваем их использование символом, call_iconvчтобы сделать вызов таким же простым, как iconvпрямой вызов . Ниже приводится общая схема, показывающая, как это можно записать:

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

(Эту последнюю логику можно очистить и обобщить; я попытался сделать каждую ее часть явной, чтобы, надеюсь, прояснить, как она работает.)

Джеймс МакНеллис
источник
3
Хорошая магия. :) Я бы поддержал, потому что похоже, что он отвечает на вопрос, но я не проверял, что он работает, и я не знаю достаточно хардкорного C ++, чтобы узнать, работает ли он, просто взглянув на него. :)
Almo
7
В качестве примечания: decltypeтребуется C ++ 11.
Michał Górny
1
+1 Lol ... так что, чтобы избежать #ifdefпроверки платформы, у вас будет 30 с лишним строк кода :) Хороший подход (хотя в последние несколько дней я беспокоился, глядя на вопросы о том, что люди, которые не действительно понимают, что они делают, начали использовать SFINAE в качестве золотого молотка ... не в вашем случае, но я боюсь, что код станет более сложным и трудным в обслуживании ...)
Дэвид Родригес - dribeas
11
@ DavidRodríguez-dribeas: :-) Я просто следую золотому правилу современного C ++: если что-то не является шаблоном, спросите себя: «Почему это не шаблон?» затем сделайте это шаблоном.
Джеймс МакНеллис
1
[Прежде чем кто-либо воспримет этот последний комментарий слишком серьезно: это шутка.
Вроде
11

Вы можете использовать следующее:

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

Вы можете пройти, const char**и в Linux / OSX он пройдет через функцию шаблона, а во FreeBSD он перейдет непосредственно кiconv .

Недостаток: он разрешит такие вызовы, iconv(foo, 2.5)которые заставят компилятор бесконечно повторяться.

Krizz
источник
2
Ницца! Я думаю, что у этого решения есть потенциал: мне нравится использование разрешения перегрузки для выбора шаблона только тогда, когда функция не является точным соответствием. Однако для работы const_castнеобходимо переместить в объект, add_or_remove_constкоторый копается в, T**чтобы определить, Tесть ли , constи добавить или удалить квалификацию, если это необходимо. Это все равно (намного) проще, чем решение, которое я продемонстрировал. Приложив немного усилий, можно также заставить это решение работать без const_cast(то есть, используя локальную переменную в вашем iconv).
Джеймс Макнеллис
Я что-то упустил? В случае, когда действительное значение не iconvявляется константой, не Tвыводится как const char**, что означает, что параметр inbufимеет тип const T, который есть const char **const, а вызов iconvв шаблоне просто вызывает сам себя? Однако, как говорит Джеймс, с соответствующей модификацией типа Tэтот трюк является основой того, что действительно работает.
Steve Jessop
Замечательное, умное решение. +1!
Linuxios
7
#ifdef __linux__
... // linux code goes here.
#elif __FreeBSD__
... // FreeBSD code goes here.
#endif

Здесь у вас есть идентификаторы всех операционных систем. Для меня нет никакого смысла пытаться делать что-то, что зависит от операционной системы, без проверки этой системы. Это как покупать зеленые брюки, но не смотреть на них.

Кровь
источник
14
Но спрашивающий прямо говорит without resorting to trying to detect the platform...
Фредерик Хамиди
1
@Linuxios: до тех пор, пока поставщики Linux или Apple не решат, что они действительно хотят следовать стандарту POSIX . Такого рода кодирование, как известно, сложно поддерживать.
Fred Foo
2
@larsmans: Linux и Mac OS X делают следовать стандарту . Ваша ссылка с 1997 года. Это FreeBSD позади.
dreamlax
3
@Linuxios: Нет, не [лучше]. Если вы действительно хотите проверить платформу, используйте autoconf или подобный инструмент. Проверяйте реальный прототип вместо того, чтобы делать предположения, которые в какой-то момент потерпят неудачу, и это не удастся для пользователя.
Michał Górny
2
@ MichałGórny: Хорошее замечание. Честно говоря, я должен просто отказаться от этого вопроса. Кажется, я не могу ничего в этом сделать.
Linuxios
1

Вы указали, что использование вашей собственной функции-оболочки допустимо. Вы также, кажется, готовы жить с предупреждениями.

Итак, вместо того, чтобы писать свою оболочку на C ++, напишите ее на C, где вы получите предупреждение только в некоторых системах:

// 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... */
                );
}
Майкл Берр
источник
1

Как насчет

static void Test(char **)
{
}

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

РЕДАКТИРОВАТЬ: конечно, «без определения платформы» - это небольшая проблема. Ой :-(

РЕДАКТИРОВАТЬ 2: хорошо, возможно, улучшенная версия?

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;
}
Кристиан Штибер
источник
Проблема в том, что на другой платформе он не будет компилироваться (то есть, если функция принимает, const char**она не сработает)
Дэвид Родригес - dribeas
1

Что о:

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

Я думаю, что это нарушает строгий псевдоним в C ++ 03, но не в C ++ 11, потому что в C ++ 11 const char**и char**есть так называемые «похожие типы». Вы не сможете избежать этого нарушения строгого псевдонима, кроме как создавая const char*, устанавливая его равным *foo, вызывая iconvс указателем на временное, а затем копируя результат обратно *fooпосле 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;
}

Это безопасно с точки зрения константной корректности, потому что все iconvработает сinbuf это увеличивает указатель, хранящийся в нем. Итак, мы «отбрасываем константу» из указателя, полученного из указателя, который не был константой, когда мы впервые его увидели.

Мы также могли бы написать перегрузку myconvи, myconv_helperкоторая берет const char **inbufи портит вещи в другом направлении, чтобы у вызывающей стороны был выбор, передать ли ей a const char**или a char**. Что, возможно, iconvдолжно было быть предоставлено вызывающей стороне в первую очередь в C ++, но, конечно, интерфейс просто скопирован с C, где нет перегрузки функций.

Стив Джессоп
источник
Код "суперпедантичности" не нужен. В GCC4.7 с текущим stdlibc ++ это необходимо для компиляции.
Конрад Рудольф
1

Обновление: теперь я вижу, что с этим можно справиться на C ++ без autotools, но я оставляю решение autoconf для людей, которые его ищут.

То, что вы ищете, iconv.m4устанавливается пакетом gettext.

AFAICS это просто:

AM_ICONV

в configure.ac, и он должен определить правильный прототип.

Затем в коде, который вы используете:

#ifdef ICONV_CONST
// const char**
#else
// char**
#endif
Михал Горный
источник
используйте для этого специализацию шаблона. см. выше.
Alexander Oh
1
Благодарность! Я уже использую автоинструменты, и это стандартный способ решения проблемы, так что он должен быть идеальным! К сожалению, мне не удалось заставить autoconf найти файл iconv.m4 (и, похоже, он не существует в OS X, в которой есть древняя версия autotools), поэтому мне не удалось заставить его работать портативно . Поиск в Google показывает, что у многих людей проблемы с этим макросом. О, автоинструменты!
ridiculous_fish
Я думаю, что в моем ответе есть некрасивый, но неопасный прием. Тем не менее, если вы уже используете autoconf, и если необходимая конфигурация существует на платформах, о которых вы заботитесь, нет реальной причины не использовать это ...
Стив Джессоп
В моей системе этот файл .m4 устанавливается gettextпакетом. Кроме того, довольно часто пакеты включают используемые макросы в m4/каталог и содержат ACLOCAL_AMFLAGS = -I m4файлы Makefile.am. Я думаю, что autopoint даже копирует его в этот каталог по умолчанию.
Michał Górny
0

Я опаздываю на эту вечеринку, но все же вот мое решение:

// 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
источник