当工件为库且标志影响C或C ++标头时,功能标志/切换

时间:2018-08-02 15:37:52

标签: c++ c architecture software-design featuretoggle

关于feature flags/toggleswhy 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相同的功能标记即可使一切正常工作
  • 公共标题中的ifdef很难看
  • 最重要的是,如果您禁用标志,则您不希望客户看到有关已禁用功能的任何信息-也许这是即将发生的事情,并且您不希望展示自己的东西,直到准备好了

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

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

5 个答案:

答案 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来达到相同的目的,但是:

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

仍有一些弊端。您必须在程序/库的任何部分尝试使用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。