如何不使用 C 头文件的声明污染全局名称空间?

How not to pollute the global namespace with declarations of a C header?

我正在尝试用 C++ 包装一个 C 库,使其成为一个现代的、高级的和惯用的 C++ 库。我想做的是使 C 对象完全不透明 and/or 直接从 C++ 代码中不可用,并 wrap/replace 它们具有更高级别的替代方案。

我面临的问题很简单:我只想将 C 头文件包含到 C++ 源代码中,这样 C++ 头文件在包含时也不会包含 C 头文件的声明,也就是说,它不会污染全局命名空间。

但头文件和源文件的正确分离似乎不允许我这样做。这是我的问题的一个非常虚拟的版本,评论会告诉你剩下的:


my_header.h:

typedef enum
{
    my_Consts_ALPHA = /* some special value */,
    my_Consts_BETA  = /* other special value */,
} my_Consts;

typedef struct
{
    // members...
} my_Type;

void
my_Type_method(my_Type *const,
               my_Enum);

my_header.hpp:

namespace my
{
    enum class Consts; // <-- This header is missing the constant values of
                       //     this enum, because its values are defined by
                       //     the C header :( 

    class Type : public my_Type // <-- The super struct is coming from the
                                //     C header, but I don't want to include
                                //     that header here :(
    {
        public:
            void
            method(Consts constant);
    };
}

my_source.cpp:

extern "C"
{
    #include "my_header.h"
}

#include "my_header.hpp"

namespace my
{
    enum class Consts
    {
        ALPHA = my_Consts_ALPHA,
        BETA  = my_Consts_BETA,
    };

    void
    Type::method(Consts constant)
    {
        my_Type_method(static_cast<my_Type *const>(this),
                       static_cast<my_Consts>(constant));
    }
}

所以我的问题是: 我是否遗漏了一些非常明显的东西?这甚至有可能实现吗?有什么我不知道的技巧吗?

我不确定它是否是合法的语言,但我认为 extern "C" 只是为了整理函数,所以只要您将它们保存在 .cpp 文件中就可以解决这个问题。

这有点亵渎神明,但它似乎适用于 gcc 4.3.5。它演示了您可以使用 C 函数,同时将它们隐藏在命名空间中。

我没有费心继承 struct_t,但它应该可以工作。我不知道你是否可以完成 enum class.

foo.h

#ifndef foo_H
#define foo_H

typedef enum {
    ALPHA,
    BETA
} enum_t;

typedef struct
{
    int i;
} struct_t;

void printit(struct_t print_me);

#endif // foo_H

foo.c

#include <stdio.h>
#include "foo.h"

void printit (struct_t print_me)
{
    printf ("Hello World %d!\n", print_me.i);
}

bar.hpp

#ifndef bar_HPP
#define bar_HPP

namespace _foo {
    // Don't need extern "C" since we're not using functions
#include "foo.h"
}

struct based_on_struct_t // : public _foo:struct_t // Do you really have to derive?  It might be possible, but it's ugly
{
    _foo::struct_t i;
    double j;
    based_on_struct_t (int _i, double _j) : j(_j) { i.i = _i; }
    void print(void); // Gonna call printit, MUST be in .cpp
};

#endif // bar_HPP

bar.cpp

namespace _foo{
extern "C" {
#include "foo.h"
}
}

#include "bar.hpp"
#include <stdio.h>

void based_on_struct_t::print (void) {
    // Call the old version...
    printit(i);

    // And do new crap
    printf ("Goodbye World %d %f\n", i.i, j);
}

driver.cpp

#include "bar.hpp"

int main (void) {
    based_on_struct_t B(10, .1);

    B.print();

    return 0;
}

演示...

$ gcc foo.c -c -O3
$ g++ foo.o bar.cpp driver.cpp
$ ./a.out
Hello World 10!
Goodbye World 10 0.100000
$

如果编写高级和惯用的 C++ 包装器的整个想法是带来安全、自动内存管理和方便的 C++ 类型,如 std::sting,我将只将 C 头文件包含到 cpp 文件中.

提供干净的惯用 C++ 接口,并仅在实现中使用 C 库。

不要害怕编写一些将 C 数据转换为 C++ 并返回的实用函数。如果 C++ class 应该保存特定于 C 的数据,并且不可能用 C++ 模拟替换它,请使用 some type erasure technique 保持干净的界面。

我不会担心这种包装导致的性能问题,直到我在探查器日志中看到它位于顶部。在大多数情况下,这不是瓶颈。

同样,拆分接口和实现通常是双赢。

更新

最初,我考虑更多的是项目特定的 C++ 接口,而不是围绕 C 库的通用 C++ 包装器。

extern "C" 包裹在命名空间中的解决方案在我看来是正确的(参见 C++11 标准的 §7.5)。但是,我从来没有在野外见过这种技术。

您可以更进一步,添加嵌套的 detail 命名空间,以免 C 类型污染 my 命名空间。这个技巧在只有头文件的库中很流行:

namespace my
{
    namespace detail
    {
        extern "C"
        {
            #include "my_header.h"
        }
    }

    enum class Consts
    {
        ALPHA = detail::my_Consts_ALPHA,
        BETA  = detail::my_Consts_BETA,
    };

    class Type : public detail::my_Type
    {
        public:
            void
            method(Consts constant);
    };
}

请注意,当您 link 使用静态库时,您不能使 C 函数完全不透明或将它们包装到单个名称空间中。他们有外在的 link 年龄,对名称空间一无所知。

namespace A {
    extern "C" void my_Type_method(my_Type *const, my_Enum);
}

namespace B {
    extern "C" void my_Type_method(my_Type *const, my_Enum);
}

extern "C" void my_Type_method(my_Type *const, my_Enum);

基本上,所有这些声明都引用同一个 C 函数。由于 C 不支持名称空间和重载,linker 通常使用函数名称作为唯一标识符(甚至忽略参数类型)。

无论如何,这种方法将有助于避免意外访问 C 接口。

在问题的评论中 @AnalPhabet suggested sarcastically, that one should use #include of a C header inside a namespace. @n.m. 确认,它实际上是一个可行的解决方案,现在我在自己的设置上对其进行了测试,幸运的是它工作得很好。

(虽然我不知道这是否是特定于实现的,但我在 g++clang++ 上都进行了测试并且它正在工作。)

它没有解决不透明问题,但至少让更难直接访问原始C数据它现在住在一个单独的 namespace 中,因此用户不能意外访问,而是自愿访问。

所以,my_header.hpp 应该是这样的:

namespace my
{
    extern "C"
    {
        #include "my_header.h"
    }

    enum class Consts
    {
        ALPHA = my_Consts_ALPHA,
        BETA  = my_Consts_BETA,
    };

    class Type : public my_Type
    {
        public:
            void
            method(Consts constant);
    };
}

因此,无论 my_header.hpp#include,用户只能按如下方式访问 C 值:

my::my_Consts_ALPHA       // The wrapped value is => my::Consts::ALPHA
my::my_Type               // The wrapped value is => my::Type
my::my_Type_method(t,..)  // The wrapped value is => t.method(..)