在C++中处理C库的匿名结构体类型

6

我们有一个庞大而古老的C++应用程序,其中包含许多遗留代码和几个用C编写的外部库。这些库很少更新,只有在发现错误并且供应商提供补丁时才会更新。上周发生了这种情况,其中一个库需要更新,当我们集成新版本时,我们发现如果不在本地修改库(显然我们在上一个版本中这样做了),我们的构建将出现以下错误消息:

non-local function ‘static E* MyCls::myFct(<anonymous struct>*)’ uses anonymous type

这是因为该库声明了许多类似于以下的句柄类型:

#define _Opaque struct {unsigned long x;} *

typedef _Opaque    Handle;
typedef _Opaque    Request;

我们在一些类的函数签名中使用了这个东西:
class MyCls {
public:
    static void* myFct(Handle handle);
    ...
}

出现上述错误是因为编译器无法为函数创建正确的名称,因为_Opaque结构体没有名称。

我们目前的解决方法是修补库头文件,显式地给结构体命名:

//#define _Opaque struct {unsigned long x;} * //Replaced by typedef below!
typedef struct __Opaque {unsigned long x;} * _Opaque;

这显然是不好的,因为我们尽可能不想触摸库。更糟糕的是,在所有函数签名中将类型转换为void*,然后将它们强制转换回各自的类型。最糟糕的是重新编写每个受影响的函数以使用纯C语言... ...
那么,我的问题是:有比修补库更好的选择吗?我是否忽略了简单的解决方案?解决这个问题的最佳方法是什么?

2
这可能是升级到C++11的一个很好的理由,因为它没有这个限制。 - ecatmur
1
@JensGustedt,没错,但我也标记了C,因为该库是用C编写的,从技术上讲,这是一个C/C++互操作性问题。 - l4mpi
@ecatmur C++11不支持匿名结构体:https://dev59.com/oGoy5IYBdhLWcg3wWcig - Morwenn
请注意,您的修复程序会破坏Handle和Request类型的类型安全性,因为它们都具有相同的类型。您需要单独创建每个类型typedef struct OpaqueHandle { unsigned long x*; } *Handle; - Adam Bowen
1
换句话说,我认为在这里对 C 的任何专业知识都没有帮助。所以需要错误的人(例如我 :) - Jens Gustedt
显示剩余2条评论
4个回答

3
您可以通过对#define行进行最小更改来实现此目的,利用7.1.3:8中的规则:声明中第一个typedef名称被用于链接目的,仅表示该类类型(或枚举类型)
#define MAKE_DUMMY2(line) dummy_ ## line
#define MAKE_DUMMY(line) MAKE_DUMMY2(line)
#define _Opaque struct {unsigned long x;} MAKE_DUMMY(__LINE__), *

这样做可以让HandleRequest等之间的连接最小化。

这个可以运行,但仍需要更改库。此外,您能解释一下您的答案吗?为什么要使用两个MAKE_DUMMY定义而不是一个? - l4mpi
1
@l4mpi 这两个定义是必要的,以便扩展 __LINE__。 预处理器很奇怪;请参见 https://dev59.com/eHI-5IYBdhLWcg3w9tdw - ecatmur
我们已经向库供应商询问是否可以包含此修复。如果他们拒绝,我们将在本地使用它...直到有人有时间和勇气测试我们是否可以安全地从gcc4.1/C++98升级到gcc4.7/C++11的编译器和C++版本。 - l4mpi

1

您可以通过声明新类型来引入名称,这些类型仅包含这些元素。在参数中使用这些类型。

namespace MON {
struct t_handle {
  Handle handle;
};

class MyCls {
public:
    static void* myFct(t_handle handle);
    ...
};
}

2
构造函数和转换运算符不能被声明,因为它们的签名将包含一个匿名类型。 - ecatmur
@ecatmur 感谢指出 (+1)。我的构造函数和转换运算符建议与 OP 使用的编译器/设置不兼容 - 已删除。 - justin
这可能是正确的做法,但我不确定是否要实施它,因为它需要对代码库的大部分进行更改。 - l4mpi

1

如果您愿意修改接口的方法,您可以比void *略微更好:

struct CHandle {
    void *p;
    CHandle(void *p): p(p) { }
};
struct CRequest {
    void *p;
    CRequest(void *p): p(p) { }
};

static CHandle make(Handle handle) { return CHandle(handle); }
static Handle get(CHandle handle) { return static_cast<Handle>(handle.p); }
static CRequest make(Request request) { return CRequest(request); }
static Request get(CRequest request) { return static_cast<Request>(request.p); }

在这里,CHandleCRequest具有链接,因此可以在您的方法签名中使用;makeget的重载具有内部链接,因此可以与匿名类型进行接口。您可以将其放在标题中,甚至是static函数。

您需要修改代码,以便例如MyCls::myFct调用库时,使用get包装参数并使用make返回值。


为什么在这种情况下声明make有效?它在其签名中使用匿名结构体...难道这不应该导致相同的名称混淆问题吗? - l4mpi
1
@l4mpi 在这种情况下是可以的,因为make是静态的,所以没有外部链接。 - ecatmur

0

这似乎有效:

class MyCls {
  public:
    typedef _Opaque MHandle;
    static void* myFct(MHandle handle) {
      return 0;
    }   
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接