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

Posted

技术标签:

【中文标题】当工件是库并且标志影响 C 或 C++ 标头时,功能标志/切换【英文标题】:Feature flags / toggles when artifact is a library and flags affect C or C++ headers 【发布时间】:2019-01-10 10:52:56 【问题描述】:

关于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 相同的功能标志来使事情正常工作 公共标头中的 ifdef 很丑 最重要的是,如果您禁用标志,您不希望客户看到有关禁用功能的任何信息 - 也许这是即将发生的事情,您不想展示您的东西,直到准备好了

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

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

【问题讨论】:

“如果你禁用你的标志,你不希望客户看到任何关于它的东西——也许它是即将发生的事情,你不想在它准备好之前展示你的东西” 这不应该是一个真正的问题:只是不要将未完成的功能从开发分支合并到发布分支与您的版本控制。 这是“长期存在的功能分支”方法,有些人更喜欢这种方法,其他人建议尽早合并并禁用功能切换功能尚未准备好迎接黄金时间的功能 - 这些讨论在链接中提到问题。 旁白:从 C 的角度来看,显式的void 应该存在于声明参数列表中:void doSomethingCool(#ifdef FEATURE_FOO int fooParam = 42 #else void #endif );。这使 OP 发布的 C 风格变得复杂。 在 C 中没有默认参数,所以这将是一个完全不同的野兽。 我真的不明白你的问题。您的函数可能有也可能没有参数,具体取决于月相?你怎么能指望任何人都能使用它? 【参考方案1】:

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

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

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

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

【讨论】:

这不仅仅是关于兼容性 - 这部分很好理解。造成最多问题的是第三个要点 - 如果您不想在标题中出现未完成的内容,#ifdef 或重载/默认值都不会真正起作用,因为这两者都在您的标题中可见。 我试图解释用户永远不需要看到那个标志。要么是因为他永远不需要查看头文件更新(二进制兼容性),要么根本不需要标志(源兼容性)。好吧,试一试。【参考方案2】:

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

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

#define FEATURE_FOO 1
void doSomethingCool();

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

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

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

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

【讨论】:

【参考方案3】:

使用前向声明

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
;

【讨论】:

【参考方案4】:

二进制兼容性不是 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。尝试解决这个问题会直接导致地狱 (Finding C++ static initialization order problems),这一定会让 N.Wirth 微笑。如果你的 Bind() 太聪明了,你会希望你没有。您可能需要小心重新进入,因为给定的客户端可能会针对不同的属性集多次绑定(用户太痛苦了)。

【讨论】:

【参考方案5】:

这就是我在纯 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 原型。

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

【讨论】:

以上是关于当工件是库并且标志影响 C 或 C++ 标头时,功能标志/切换的主要内容,如果未能解决你的问题,请参考以下文章

三坐标检测之坐标系建立原则及分类

C++ <mutex> 标头在执行并发时是不是使用硬件支持或纯粹是基于算法的解决方案

在 C 或 C++ 中包含标头的正确方法

有没有办法将 c++ 编译器标志设为默认值

不带 -I 标志的链接 Boost 标头(来自 nuwen.net 的 MinGW Distro)

当 Visual Studio 2017 与 std 标头不兼容时,如何使用最高警告级别(墙)?