关于feature flags/toggles和why you would use them的讨论很多,但是关于实现它们的大多数讨论都围绕(网络或客户端)应用程序进行。如果您的产品/工件是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
相同的功能标记即可使一切正常工作在文件上运行预处理器以获取要分发的标头实际上是行不通的,因为这不仅会作用于功能标志,还会执行预处理器所做的所有其他事情。
没有这些缺陷的技术解决方案是什么?
答案 0 :(得分:2)
由于版本控制,这种粘糊糊最终存在于代码库中。主题广泛,答案很少。但是您当然想避免使其变得比原来更困难。专注于要提供的种类兼容性。
仅当您需要 binary 兼容性时,才需要使用摘要中建议的语法。它使库与客户端代码中的doSomethingCool()调用兼容(不传递参数),而无需编译该客户端代码。换句话说,客户端程序员除了复制更新的.dll或.so文件外,什么也不做,不需要任何更新的头文件,正确使用功能标志完全是您的负担。二进制兼容性很难可靠地实现,超出了争执的范围,容易出错。
但是您实际上要说的是源兼容性,您确实为用户提供了更新的标头,并且他重建了代码以使用库更新。在这种情况下,您不需要,C ++编译器本身可以确保传递参数,它将是42。在您端或用户端根本不需要标志
另一种方法是通过提供重载。换句话说,doSomethingCool()和doSomethingCool(int)函数都可以。客户端程序员将继续使用原始的重载,直到他准备好继续前进为止。当函数主体必须进行太多更改时,您也倾向于重载。如果这些功能不是虚拟的,那么它甚至可以提供链接兼容性,这在某些情况下可能很有用。不需要功能标志。
答案 1 :(得分:2)
我会说这是一个相对较广泛的问题,但是我要花两分钱。
首先,您确实要从实现中分离公共头(源头和内部头,如果有的话)。安装的公共标头(例如,在/usr/include
处)应包含函数声明,并且最好包含一个常量布尔值,以通知客户端该库是否具有编译的特定功能,如下所示:
#define FEATURE_FOO 1
void doSomethingCool();
通常会生成此类标头。 Autotools是GNU / Linux中为此目的的事实上标准工具。否则,您可以编写自己的脚本来执行此操作。
为完整起见,在.c文件中,您应该拥有
void doSomethingCool(
#ifdef FEATURE_FOO
int fooParam = 42
#endif
);
由分发工具来确保已安装的标头和库二进制文件保持同步。
答案 2 :(得分:0)
使用前向声明
Hide implementation by using a pointer (Pimpl idiom)
上一个链接引用的此代码ID:
// Foo.hpp
class Foo {
public:
//...
private:
struct Impl;
Impl* _impl;
};
// Foo.cpp
struct Foo::Impl {
// stuff
};
答案 3 :(得分: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为您提供的属性集提供/构造适当的(功能,标记)对;否则失败。注意,Bind是唯一需要了解* funcs,* stamp;的多种布局的位。因此它可以透明地为此简化版本的问题提供可靠的界面。
如果您真的想花哨的话,可以通过重写dlopen / dlsym为您准备的PLT来达到相同的目的,但是:
仍有一些弊端。您必须在程序/库的任何部分尝试使用Bind之前调用Bind。尝试解决该问题会直接导致地狱(Finding C++ static initialization order problems),这必须使N.Wirth露出微笑。如果您对Bind()太聪明了,那会希望您没有。您可能需要谨慎对待重入权限,因为给定的客户端可能会为不同的属性集多次绑定(用户很痛苦)。
答案 4 :(得分:0)
这就是我要在纯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。