关于功能标志/切换以及为什么要使用它们,有很多讨论,但大多数关于实现它们的讨论都围绕着(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很难看
- 最重要的是,如果您禁用了标志,您不希望客户端看到任何关于禁用功能的信息-也许这是即将到来的事情,在准备好之前您不想显示您的东西
在文件上运行预处理器以获取用于分发的头并不能真正起作用,因为这不仅会作用于功能标志,还会执行预处理器所做的所有其他操作。
没有这些缺陷的技术解决方案是什么?
由于版本控制,这种goo最终会出现在代码库中。广泛的话题,很少有令人满意的答案。但你当然想避免让它变得更加困难。专注于你想要提供的类兼容性。
只有当您需要二进制兼容性时,才需要代码段中提出的语法。它使库与客户端代码中的doSomethingCool()调用保持兼容(不传递参数),而不必编译该客户端代码。换句话说,客户端程序员除了复制更新后的.dll或.so文件外,什么都不做,不需要任何更新的头,而且完全由您负责正确地获取功能标志。二进制兼容性很难可靠地实现,除了争旗之外,很容易出错。
但实际上,您所说的是源代码兼容性,您确实为用户提供了一个更新的头,他会重新构建代码以使用库更新。在这种情况下,不需要特性标志,C++编译器本身会确保传递一个参数,它将是42。无论是在您端还是在用户端,都不需要任何标志。
另一种方法是提供过载。换句话说,doSomethingCool()和doSometingCool(int)函数。客户端程序员一直使用原始的重载,直到他准备好继续前进。当函数体必须改变太多时,您也喜欢重载。如果这些功能不是虚拟的,那么它甚至提供了链接兼容性,在某些特定情况下可能很有用。不需要任何功能标志。
我认为这是一个相对宽泛的问题,但我会花两分钱。
首先,您确实希望将公共标头与实现(源标头和内部标头,如果有的话)分开。安装的公共头(例如,在/usr/include
)应该包含函数声明,最好是一个常量布尔值,以通知客户端库是否编译了某个功能,如下所示:
#define FEATURE_FOO 1
void doSomethingCool();
通常会生成这样的标头。Autotools是GNU/Linux中用于此目的的事实上的标准工具。否则,您可以编写自己的脚本来完成此操作。
为了完整起见,在.c文件中应该有
void doSomethingCool(
#ifdef FEATURE_FOO
int fooParam = 42
#endif
);
您的分发工具也可以保持已安装的头文件和库二进制文件的同步。
使用前向声明
使用指针隐藏实现(Pimpl习惯用法)
这个代码id引用自上一个链接:
// Foo.hpp
class Foo {
public:
//...
private:
struct Impl;
Impl* _impl;
};
// Foo.cpp
struct Foo::Impl {
// stuff
};
二进制兼容性不是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来实现同样的效果,但是:
- 你正在极大地扩大你的攻击面
- 你增加了很多复杂性,却收效甚微
- 您正在添加平台/体系结构特定的代码,但没有任何代码是有保证的
仍有一些缺点。你必须在程序/库的任何部分尝试使用Bind之前调用它。试图解决这个问题会直接导致地狱(发现C++静态初始化顺序问题),这一定会让N.Wirth会心一笑。如果你的Bind()太聪明了,你会希望你没有。您可能需要小心重新登录,因为给定的客户端可能会为不同的属性集绑定多次(用户很痛苦)。
这就是我在纯C.中管理它的方式
首先,我会将它们打包在一个32/64位长的无符号int中,以使它们尽可能紧凑。
第二步是只在库编译中使用的私有头,在这里我将定义一个宏来创建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函数原型。
为什么要使用特性标志的定义?功能标志应该使您能够在运行时打开和关闭功能,而不是在编译时。
在代码中,您将使用基于特性标志选择的接口和具体类,尽早地对实现进行案例分析。
如果头文件的用户不能访问特性标志,那么创建不分发的头文件,这些头文件只包含在实现c/cpp文件中。然后,当编译它们链接到的库时,您可以翻转私有标头中的标志。
如果在准备发布之前一直将特性保留在内部,则可以将特性标志移动到公共标头中,或者完全删除特性标志并切换到使用新实现。
如果你想要这个编译时间,那就是一个糟糕的例子:
public_class.h
阶级的东西{公共:void DoSomething();}
private_class_feature1.h#定义USE_FEATURE_1
类NewFeatureImp{公共:静态空隙CoolNewWay1();}
public_class.cpp#包括"public_class.h"#包括"private_class_feature1.h">
void Thing::DoSomething(){#ifdef USE_FEATURE_1NewFeatureImpl::CoolNewWay();#其他//正则impl#endif}