【问题标题】:Adding namespaces to C++ implementations that have a C header将命名空间添加到具有 C 标头的 C++ 实现
【发布时间】:2011-12-28 07:04:15
【问题描述】:

我们有一个包含 C 和 C++ 代码的大型项目。

对于每个 C++ 实现,除了 C++ 头文件外,我们通常还提供一个 C 头文件以允许 .c 文件也可以使用功能。

所以,我们的大部分文件都是这样的:

foo.hpp:

class C { 
    int foo();
};

foo.h:

#ifdef __cplusplus
extern "C" {
    typedef struct C C;  // forward declarations
#else
    class C;
#endif

    int foo( C* );               // simply exposes a member function
    C*  utility_function( C* );  // some functionality *not* in foo.hpp  

#ifdef __cplusplus
}
#endif

foo.cpp:

int C::foo()  { /* implementation here...*/ }

extern "C"
int foo( C *p ) { return p->foo(); }

extern "C"
C*  utility_function ( C* ) { /* implementation here...*/ }

问题:

假设我想像这样向类添加一个命名空间:

foo.hpp:

namespace NS {
    class C { 
        int foo();
    };
}

在 C 标头中遵循的最佳方案是什么?

我已经考虑了几个选项,但我正在寻找最优雅、安全且易于阅读的选项。有没有标准的使用方式?


以下是我考虑过的选项: (为简单起见,我省略了 extern "C" 构造)

  • 选项 1: 通过在每个标头中添加一些代码来欺骗编译器:

foo.h

#ifdef __cplusplus
    namespace NS { class C; }  // forward declaration for C++ 
    typedef NS::C NS_C;
#else
    struct NS_C;  // forward declaration for C
#endif

int foo( NS_C* );
NS_C*  utility_function( NS_C* );

这给标头增加了一些复杂性,但保持实现不变。


  • 选项 2: 用 C-struct 包装命名空间:

    保持标题简单,但使实现更复杂:

foo.h

struct NS_C;  // forward declaration of wrapper (both for C++ and C)

int foo( NS_C* );
NS_C*  utility_function( NS_C* );

foo.cpp

namespace NS {
    int C::foo() { /* same code here */ }
}

struct NS_C {     /* the wrapper */
    NS::C *ptr;
};

extern "C" 
int foo( NS_C *p ) { return p->ptr->foo(); }

extern "C"
NS_C *utility_function( NS_C *src ) 
{
    NS_C *out = malloc( sizeof( NS_C ) );  // one extra malloc for the wrapper here...
    out->ptr = new NS::C( src->ptr );
    ...
}

这些是唯一的方案吗?这些中是否有任何隐藏的缺点?

【问题讨论】:

  • 这很模糊。 C 的奇怪 ifdef 声明闻起来像是几英里外未定义的行为;但更重要的是,我看不到重点:C 程序将用foo() 做什么?它周围没有任何有用的C-pointer,所以那里发生了什么?
  • 最初 class C 在 .c 文件中实现为 struct C。 .h 文件始终包含foo(C*) 函数。 foo.h 的客户从不知道struct C 的内容。它是“C”术语的封装。在某些时候,struct C 需要发展,我们将其更改为 class C 并将其实现移至 .cpp 文件中。从来没有 C 程序对 C 对象做任何事情。它总是引用C-pointer。这对于超过 10 年的代码来说是典型的。我希望这能澄清我的意图。
  • 在这种情况下,我只需让 C 函数接受 void* 参数,并在 C++ 实现中执行强制转换:`inf foo(void * p) { return reinterpret_cast (p)->foo(); “从未使用过”的 C 结构类型简直令人困惑,并且使事情变得更糟。如果您想要一个永远不会取消引用的指针,请使用 void 指针。
  • 虽然从标准的角度来看,您的提议在技术上是正确的,但我们广泛使用我所描述的技术,因为它提供了void* 无法提供的类型安全性。它也更具可读性(至少如果有人解释了这个成语)。我们已经使用了这两个成语,并且已经习惯了这个,因为我们发现我们的错误更少,而且读起来更好。

标签: c++ c namespaces


【解决方案1】:

我发现以某种方式分解代码更容易,这样foo.h 只包含最少量的 C++ 细节,而foo.hpp 负责处理细节。

foo.h 文件包含 C API,不应直接包含在 C++ 代码中:

#ifndef NS_FOO_H_
#define NS_FOO_H_

// an incomplete structure type substitutes for NS::C in C contexts
#ifndef __cplusplus
typedef struct NS_C NS_C;
#endif

NS_C *NS_C_new(void);
void NS_C_hello(NS_C *c);

#endif

foo.hpp 文件包含实际的 C++ API,并负责将 foo.h 包含到 C++ 文件中:

#ifndef NS_FOO_HPP_
#define NS_FOO_HPP_

namespace NS {
    class C {
    public:
        C();
        void hello();
    };
}

// use the real declaration instead of the substitute
typedef NS::C NS_C;
extern "C" {
#include "foo.h"
}

#endif

实现文件foo.cpp是用C++编写的,因此包含foo.hpp,它也引入了foo.h

#include "foo.hpp"
#include <cstdio>

using namespace NS;

C::C() {}

void C::hello() {
    std::puts("hello world");
}

C *NS_C_new() {
    return new C();
}

void NS_C_hello(C *c) {
    c->hello();
}

如果您不想让 C API 可用于 C++ 代码,您可以将相关部分从 foo.hpp 移动到 foo.cpp

作为使用 C API 的示例,一个基本文件 main.c

#include "foo.h"

int main(void)
{
    NS_C *c = NS_C_new();
    NS_C_hello(c);
    return 0;
}

此示例已使用以下编译器标志在 MinGW 版本的 gcc 4.6.1 中进行了测试:

g++ -std=c++98 -pedantic -Wall -Wextra -c foo.cpp
gcc -std=c99 -pedantic -Wall -Wextra -c main.c
g++ -o hello foo.o main.o

代码假定NS::C *struct NS_C * 类型具有兼容的表示和对齐要求,几乎所有地方都应如此,但据我所知,C++ 标准不保证(请随时纠正我如果我在这里错了)。

从 C 语言的角度来看,当您在技术上通过不兼容类型的表达式调用函数时,代码实际上调用了未定义的行为,但这是没有包装结构和指针转换的互操作性的代价:

由于 C 不知道如何处理 C++ 类指针,因此可移植的解决方案是使用 void *,您可能应该将其包装在一个结构中以恢复某种程度的类型安全:

typedef struct { void *ref; } NS_C_Handle;

这将在具有统一指针表示的平台上添加不必要的样板:

NS_C_Handle NS_C_new() {
    NS_C_Handle handle = { new C() };
    return handle;
}

void NS_C_hello(NS_C_Handle handle) {
    C *c = static_cast<C *>(handle.ref);
    c->hello();
}

另一方面,它会去掉 foo.h 中的#ifndef __cplusplus,所以它实际上并没有那么糟糕,如果你关心可移植性,我会说去吧.

【讨论】:

  • 非常感谢您的回答。不过,您所说的未定义行为问题与命名空间无关,对吧?我的意思是这些问题的存在是因为我们在 C 中将类指针称为结构指针,而不是因为添加了命名空间。或者我们在这里引入了任何额外的问题?顺便说一句,您所描述的这种模式/方案有多受欢迎?看起来你现在还没有编出来,但是有很多人/团体使用它吗?
  • @Grim:是的,UB 与命名空间无关,而是类型兼容性;为我的答案添加了便携式解决方案;至于我的分解方案的受欢迎程度:最常见的解决方案可能是有一个充满#ifdefs 的标头,我一点也不喜欢;我首先是一名 C 程序员,所以如果我查看一个与 C 兼容的头文件,我不想涉足 C++ 代码的污泥以获得我感兴趣的位(更多...)跨度>
  • 如果您已经有单独的标头,那么在 .h 文件中使用 #ifdef __cplusplus \n extern "C" { 或在 .hpp 中使用 extern "C" { \n #include 并不重要 文件 - 我只是觉得后者更容易一些......
【解决方案2】:

你的标题全乱了。

你可能想要更多类似的东西:

struct C;

#ifdef __cplusplus
extern "C" {
#else
typedef struct C C;
#endif

/* ... */

#ifdef __cplusplus
}
#endif

【讨论】:

  • 看来我的沟通不够好:标头可能包含在 .cpp 文件中,因此我需要在 __cplusplus 下将 C 声明为 class C,否则声明为 struct C。否则无法编译。
【解决方案3】:

我不完全理解您要做什么,但这可能会有所帮助:

如果您希望 C 仍然可以访问它,请执行以下操作:

void foo();

namespace ns {
  using ::foo;
}

或者使用宏:

#ifdef __cplusplus
#define NS_START(n) namespace n {
#define NS_END }
#else
#define NS_START(n)
#define NS_END
#endif

NS_START(ns)
void foo();
NS_END

【讨论】:

  • 第一个代码块是否意味着“将 foo() 移动到命名空间 ns”?如果是这样,这是否意味着 foo() 现在同时存在于 ns 全局命名空间中?谢谢,Pubby。
  • @GrimFandango 第一个允许您使用foo()ns::foo()。这就是 C 标头在 C++ 中的工作方式 - 您可以使用 printf()std::printf()
猜你喜欢
  • 2010-11-28
  • 1970-01-01
  • 2012-12-26
  • 2011-11-27
  • 2016-07-08
  • 1970-01-01
  • 2011-06-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多