如何在某些平台上可移植地调用一个采用char **且在其他平台上采用const char **的C ++函数?


91

在我的Linux(和OS X)计算机上,该iconv()函数具有以下原型:

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

在FreeBSD上,它看起来像这样:

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

我希望我的C ++代码可以在两个平台上构建。对于C编译器,传递char**for const char**参数(反之亦然)通常仅发出警告;反之亦然。但是在C ++中,这是一个致命错误。因此,如果传递a char**,它将无法在BSD上进行编译,如果传递a 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 2012年

24
在FreeBSD上记录错误。的POSIX实现iconv要求inbuf为非常量。
dreamlax 2012年

3
像这样强制转换函数是不可移植的。
乔纳森·格林斯潘

2
@dreamlax:提交错误报告不太可能产生效果;FreeBSD的当前版本显然已经iconv没有了constsvnweb.freebsd.org/base/stable/9/include/…–
Fred Foo

2
@larsmans:很高兴知道!我从未使用过FreeBSD,但很高兴知道最新版本支持最新标准。
dreamlax 2012年

Answers:


57

如果您只想对某些const问题视而不见,则可以使用模糊区分的转换,即使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);

无论iconv的第二个参数要求是什么,sloppy()都将a char**或a const char*转换为a char**或a const char*

更新:更改为使用const_cast并调用sloppy而不是as cast。


这工作得很好,并且似乎不需要C ++ 11就可以安全,简单地工作。我要去!谢谢!
ridiculous_fish 2012年

2
正如我在回答中所说的那样,我认为这违反了C ++ 03中的严格别名,因此从某种意义上讲,它确实需要C ++ 11。如果有人想捍卫这一点,我可能是错的。
史蒂夫·杰索普

1
请不要鼓励使用C ++进行C样式转换。除非我错了,否则您可以sloppy<char**>()在那里直接调用初始化程序。
米哈尔戈尔诺-

当然,它仍然与C样式转换相同,但使用其他C ++语法。我想这可能会阻止读者在其他情况下使用C样式强制转换。例如,(char**)&in除非您首先为定义typedef,否则C ++语法不适用于强制类型转换char**
史蒂夫·杰索普

好骇客。为了完整起见,您可以使它要么(a)始终采用const char * const *,假设不应该对变量进行更改,要么(b)通过任何两种类型进行参数化并使其在它们之间进行const转换。
Jack V.

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 2012年

7
注意:decltype需要C ++ 11。
米哈尔戈尔诺-

1
+1大声笑...因此,为了避免#ifdef检查平台,您最终会得到30条奇数行的代码:)虽然,这是一种不错的方法(尽管最近几天我一直在担心关于SO的问题,以至于人们不会真正了解他们在做什么使用SFINAE作为金锤......不是你的情况下已经开始,但我担心的代码会变得更加复杂和难以维护...)
大卫·罗德里格斯- dribeas

11
@DavidRodríguez-dribeas::-)我只是遵循现代C ++的黄金法则:如果某些东西不是模板,请问问自己:“为什么这不是模板?” 然后使其成为模板。
James McNellis 2012年

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)这样的调用使编译器无限循环。


2
真好!我认为该解决方案具有潜力:我喜欢仅在函数不完全匹配时才使用重载分辨率来选择模板。但是,要进行工作,const_cast需要将迁移到,add_or_remove_const然后深入挖掘,T**以检测是否T存在const并适当地添加或删除资格。这将比我已经证明的解决方案更简单。通过一些工作,也可以使该解决方案在不const_cast使用的情况下工作(例如,通过在您的中使用局部变量iconv)。
James McNellis

我错过了什么吗?在实数iconv是非常量的情况下,不T推导为const char**,这意味着参数的inbuf类型为const T,即const char **const,并且iconv模板中的调用只是调用自身?就像James所说的那样,对类型进行适当的修改后,T此技巧便成为可行的基础。
史蒂夫·杰索普

很棒的解决方案。+1!
Linuxios

7
#ifdef __linux__
... // linux code goes here.
#elif __FreeBSD__
... // FreeBSD code goes here.
#endif

在这里,您具有所有操作系统的ID。对我来说,没有检查该系统就没有必要尝试做一些依赖于操作系统的事情。这就像买绿色的裤子,但不看它们。


13
但发问者明确地说without resorting to trying to detect the platform……
弗雷德里克·哈米迪

1
@Linuxios:直到Linux厂商或苹果决定,他们想跟着POSIX标准。众所周知,这种编码很难维护。
弗雷德·富

2
@larsmans:Linux和Mac OS X 确实遵循该标准。您的链接来自1997年。背后是FreeBSD。
dreamlax 2012年

3
@Linuxios:不,不是[更好]。如果您确实要进行平台检查,请使用autoconf或类似工具。检查实际的原型,而不是做一些会在某个时候失败并且对用户失败的假设。
米哈尔戈尔诺-

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的指针到临时,然后将结果复制回*fooconst_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;
}

从const正确性的POV来看这是安全的,因为所有iconv操作inbuf都是增加存储在其中的指针。因此,我们从最初看到非常量指针的指针中“抛出了常量”。

我们也可以这样写的过载myconvmyconv_helper称取const char **inbuf和混乱的事情有关在其他方向,从而使调用者有选择是否在一传const char**或一char**。可以说,这iconv应该是在C ++中首先应该给调用方的,但是当然,接口只是从C复制而来的,这里没有函数重载。


不需要“超级pedantry”代码。在具有当前stdlibc ++的GCC4.7上,您需要进行编译。
康拉德·鲁道夫2012年

1

更新:现在我看到无需自动工具就可以用C ++处理它,但是我将autoconf解决方案留给了寻找它的人们。

您要查找的是iconv.m4由gettext软件包安装的。

AFAICS只是:

AM_ICONV

在configure.ac中,它应该检测到正确的原型。

然后,在代码中使用:

#ifdef ICONV_CONST
// const char**
#else
// char**
#endif

为此使用模板专门化。往上看。
Alex

1
谢谢!我已经使用过自动工具,这似乎是解决此问题的标准方法,所以它应该是完美的!不幸的是,我无法获得autoconf来查找iconv.m4文件(并且OS X似乎不存在,该文件具有较早版本的自动工具),因此我无法使其可移植地工作。谷歌搜索显示很多人对此宏有麻烦。哦,自动工具!
ridiculous_fish

我认为我的回答有一个丑陋但没有风险的技巧。不过,如果您已经在使用autoconf,并且您关心的平台上已经存在必要的配置,则没有真正的理由不使用此配置...
Steve Jessop 2012年

在我的系统上,.m4文件是通过gettext软件包安装的。此外,它是很常见的包包括在使用宏m4/目录,并ACLOCAL_AMFLAGS = -I m4Makefile.am。我认为默认情况下autopoint甚至会将其复制到该目录。
米哈尔戈尔诺-

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);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.