【问题标题】:How not to pollute the global namespace with declarations of a C header?如何不使用 C 标头的声明污染全局命名空间?
【发布时间】:2016-01-20 16:36:13
【问题描述】:

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

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

但看起来头文件和源文件的正确分离不允许我这样做。这是我的问题的一个非常愚蠢的版本,cmets 会告诉你其余的:


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

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

【问题讨论】:

  • namespace m00 {#include "myheader.h"}(是的,这有点讽刺)
  • the pimpl idiom怎么样?
  • 您正试图在 cpp 库的接口中重用 c 库中的类型。只要是这样,您显然无法隐藏这些类型。
  • Pimpl 是个糟糕的主意。扼杀了很多优化。
  • @PeterVaro 我记错了,使用范围枚举,您无法获得基础值,句点。 (顺便说一句:我可以在作用域枚举和仅包含非作用域枚举定义的结构之间找到的唯一成本差异是类型安全方面——我假设你关心的是,struct 的变量 类型通常是一个字节,除了它作为基类包含在另一个类中,在这种情况下the Base Class Optimization comes out to play,但你只会使用枚举的实例化......)

标签: c++ c++11 header namespaces wrapper


【解决方案1】:

在问题@AnalPhabet 的cmets 中讽刺地建议,应该在namespace 内使用C 标头的#include@n.m. 确认,它实际上是一个可行的解决方案,现在我在自己的设置上对其进行了测试,幸运的是它运行良好。

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

它不能解决 不透明 问题,但至少它使 有点难 直接访问原始 C 数据,因为它存在于单独的 @987654327 中@now,因此用户不能意外访问,而是自愿访问。

所以,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'd,用户只能访问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(..)

【讨论】:

  • 谢谢你们:@AnalPhabet 和 @n.m.!
  • @AnalPhabet 怎么样?
  • 命名空间中定义的类型、动词、功能。是size_tstd::size_t::size_t 还是mynaemspaec::size_t
【解决方案2】:

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

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

不要害怕编写几个将 C 数据转换为 C++ 并返回的实用函数。如果 C++ 类应该保存 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);
    };
}

考虑到当您与静态库链接时,您不能使 C 函数完全不透明或将它们包装到单个命名空间中。他们有外部链接,对命名空间一无所知。

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 不支持命名空间和重载,链接器通常使用函数名作为唯一标识符(甚至忽略参数类型)。

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

【讨论】:

  • 感谢您的回复,尽管您只是总结了我在这里尝试做的事情。但是当我尝试时,我遇到了一些问题,其中一个就是上面的问题,很遗憾你没有回答。
  • @PeterVaro 嗯...如果您不在 C++ 接口中使用 C 结构和枚举,则无需将 .h 包含到 .hpp 文件中。我错过了什么?
  • 这正是我想要实现的,这正是我所问的:如何不将原始 .h 包含到 .hpp => 但因为枚举和类都需要一些来自它,看起来这是不可避免的......无法在没有 C 对应的情况下转发声明包装类型......
  • @PeterVaro 我将在 .hpp 文件中定义 C++ 枚举类,并将函数添加到 .cpp 中,将 C++ 枚举转换为 C 枚举并返回。我不会在 C++ 枚举定义中使用 C 枚举值。 C++ 类与 C 结构相同。我使用 C++ 包装器添加了指向我的小型 C 库的链接。
  • 因此,您会将所有转换推送到运行时,而不是转换时间 => 在这种情况下可以而且应该避免(不仅因为性能开销很小,而且因为它的不必要的性质)
【解决方案3】:

我不确定它是否在语言上合法,但我认为 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
$

【讨论】:

  • 在您回答的这个阶段,它不能解决任何问题 => 您将 bar.hpp 包含到用户代码中,其中还包含 foo.h,因此所有 C 声明都可以访问。我需要一个解决方案,我只将foo.h 包含到bar.cppbar.hpp 保持干净,不直接包含任何与C 相关的东西。困难的部分是:枚举和类都需要一些东西来进行合理的前向声明..
猜你喜欢
  • 2014-04-25
  • 1970-01-01
  • 2019-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多