5

关于功能标志/切换以及为什么要使用它们存在很多讨论,但大多数关于实现它们的讨论都围绕(Web 或客户端)应用程序进行。如果您的产品/工件是 C 或 C++ 库,并且您的公共标头受到标志的影响,您将如何实现它们?

“天真”的做法并不真正奏效:

/// Does something
/**
 * Does something really cool
#ifdef FEATURE_FOO
 * @param fooParam describe param for foo
#endif
 */
void doSomethingCool(
#ifdef FEATURE_FOO
    int fooParam = 42
#endif
);

你不会想运送这样的东西。

  • 您发布的库是为特定功能标志组合构建的,客户端不需要#define相同的功能标志来使事情正常工作
  • 公共标头中的 ifdef 很难看
  • 最重要的是,如果您禁用标志,您不希望客户看到有关禁用功能的任何信息- 也许这是即将发生的事情,您不想在准备好之前展示您的东西

在文件上运行预处理器以获取用于分发的标头实际上并不起作用,因为这不仅会作用于功能标志,还会执行预处理器所做的所有其他事情。

没有这些缺陷的技术解决方案是什么?

4

5 回答 5

2

我会说这是一个相对广泛的问题,但我会投入两分钱。

首先,您真的想将公共标头与实现(源和内部标头,如果有的话)分开。安装的公共头文件(例如 at /usr/include)应该包含函数声明,最好是一个常量布尔值,以通知客户端库是否具有编译的特定功能,如下所示:

#define FEATURE_FOO 1
void doSomethingCool();

通常会生成这样的报头。Autotools是GNU/Linux 中用于此目的的事实上的标准工具。否则,您可以编写自己的脚本来执行此操作。

为了完整起见,在 .c 文件中你应该有

void doSomethingCool(
#ifdef FEATURE_FOO
    int fooParam = 42
#endif
);

保持已安装的标头和库二进制文件同步也取决于您的分发工具。

于 2018-08-07T07:20:44.897 回答
2

由于版本控制,这种粘性最终会出现在代码库中。广泛的主题,很少有快乐的答案。但你当然想避免让它变得比它需要的更困难。专注于您想要提供的兼容性

仅当您需要二进制兼容性时,才需要片段中建议的语法。它使库与客户端代码中的 doSomethingCool() 调用兼容(不传递参数),而无需编译该客户端代码。换句话说,客户端程序员除了复制更新的 .dll 或 .so 文件之外什么都不做,不需要任何更新的标头,并且完全是您的负担来获得正确的功能标志。二进制兼容性很难可靠地实现,除了争吵之外,很容易出错。

但是您实际上在谈论的是源兼容性,您确实为用户提供了更新的标头,并且他重建了他的代码以使用库更新。在这种情况下,您不需要功能标志,C++ 编译器本身会确保传递一个参数,它将是 42。根本不需要标志,无论是在您端还是在用户端。

另一种方法是提供重载。换句话说,一个 doSomethingCool() 和一个 doSomethingCool(int) 函数。客户端程序员继续使用原来的重载,直到他准备好继续前进。当函数体必须改变太多时,你也喜欢重载。如果这些功能不是虚拟的,那么它甚至提供链接兼容性,在某些特定情况下可能很有用。不需要功能标志。

于 2018-08-05T10:14:46.310 回答
0

二进制兼容性不是 C++ 的强项,它可能不值得考虑。对于 C,您可能会构建类似接口类的东西,因此您对库的第一次接触类似于:

struct kv {
     char *tag;
     int   val;
};
int Bind(struct kv *compat, void **funcs, void **stamp);

您现在可以访问图书馆:

#define MyStrcpy(src, dest)  (funcs->mystrcpy((stamp)(src),(dest)))

约定是 Bind 为您提供的属性集提供/构造一个适当的 (func, stamp) 对;如果不能,则失败。请注意,Bind 是唯一需要了解 *funcs,*stamp; 多个布局的位。所以它可以透明地为这个问题的简化版本提供健壮的接口。

如果您想变得非常花哨,则可以通过重写 dlopen/dlsym 为您准备的 PLT 来实现相同的目的,但是:

  1. 您正在大大扩展您的攻击面。
  2. 您增加了很多复杂性,但收益却很少。
  3. 您正在添加没有保证的平台/体系结构特定代码。

一些缺点仍然存在。您必须在程序/库的任何部分尝试使用它之前调用 Bind。试图解决这个问题会直接导致地狱(寻找 C++ 静态初始化顺序问题),这必须让 N.Wirth 微笑。如果你的 Bind() 太聪明了,你会希望你没有。您可能要小心重新进入,因为给定的客户端可能会为不同的属性集绑定多次(用户是如此痛苦)。

于 2018-08-07T14:48:19.760 回答
0

使用前向声明

使用指针隐藏实现(Pimpl idiom)

此代码 id 引用自上一个链接:

// Foo.hpp
class Foo {
public:

    //...

private:
    struct Impl;
    Impl* _impl;
};

// Foo.cpp
struct Foo::Impl {
    // stuff
};
于 2018-08-07T12:05:51.490 回答
0

这就是我在纯 C 中管理它的方式。

首先,我会将它们打包成一个 32/64 位长的无符号整数,以使它们尽可能紧凑。

第二步,仅在库编译中使用的私有头文件,我将在其中定义一个宏来创建 API 函数包装器和内部函数:

#define CoolFeature1 0x00000001    //code value as 0 to disable feature
#define CoolFeature2 0x00000010
#define CoolFeature3 0x00000100
.... // Other features

#define Cool CoolFeature1 | CoolFeature2 | CoolFeature3 | ... | CoolFeature_n

#define ImplementApi(ret, fname, ...)    ret fname(__VA_ARGS__)  \
                                         { return Internal_#fname(Cool, __VA_ARGS__);}  \
                                         ret Internal_#fname(unsigned long Cool, __VA_ARGS__)
#include "user_header.h"    //Include the standard user header where there is no reference to Cool features

现在我们有一个带有标准原型的包装器,它将在用户定义标题中可用,以及一个内部版本,它保留一个附加标志组以指定可选功能。

使用宏编码时,您可以编写:

ImplementApi(int, MyCoolFunction, int param1, float param2, ...)
{
    // Your code goes here
    if (Cool & CoolFeature2)
    {
        // Do something cool
    }
    else
    {
        // Flat life ...
    }
    ...
    return 0;
}

在上述情况下,您将获得 2 个定义:

int Internal_MyCoolFunction(unsigned long Cool, int param1, float param2, ...);
int MyCoolFunction(int param1, float param2, ...)

如果您正在分发动态库,您最终可以在宏中为 API 函数添加用于导出的属性。

如果宏的定义是在编译器命令行上完成的,您甚至可以使用相同的定义头ImplementApi,在这种情况下,头中的以下简单定义就可以了:

#define ImplementApi(ret, fname, ...)    ret fname(__VA_ARGS__);

最后一个将只生成导出的 API 原型。

当然,这个建议并不详尽。您可以进行更多调整以使定义更加优雅和自动化。即包括一个带有函数列表的子标题,以便仅为用户创建 API 函数原型,以及为开发人员创建内部和 API。

于 2018-08-10T09:25:36.780 回答