零运行时成本功能标志

时间:2018-07-03 13:05:44

标签: c++

目标是拥有一个Feature flag system,并且没有运行时成本。一个简单的C99解决方案是:

C99:

#include <stdio.h>

#define flag_1 1

int main()
{
#if flag_1
    printf("Active\n");
#else
    printf("InActive\n");
#endif

    return 0;
}

并不是说这里的C ++ 17解决方案看起来很优雅:

#include <iostream>

constexpr  bool tag_flag_1 = true;
constexpr  bool tag_flag_2 = true;


int main()
{
    if constexpr(tag_flag_1)
    {
        std::cout << "Active" << std::endl;
    }
    else
    {
        std::cout << "InActive" << std::endl;
    }
    return 0;
}

但是不起作用,因为“ if constexpr”构造仅在“ if”构造有效的地方有效。例如,此代码无效:

if constexpr(tag_flag_1)
{
    class foo
    {

    };
}

这是:

#if tag_flag_1
    class foo
    {

    };
#endif

C99解决方案问题:

键入:

if constexpr(flag_not_exists)

会导致编译错误,而:

#if flag_not_exists

不会。

当然,人们总是可以在C99中编写这种替代的繁琐解决方案:

#include "stdio.h"

#define flag_1 0

int main()
{
#if flag_1
    printf("Active\n");
#elif defined(flag_1)
    printf("InActive\n");
#else
#error undefined_flag
#endif

    return 0;
}

问题:

是否有一种优雅的方法来确保使用不存在的功能标志(例如,拼写错误)会导致编译错误?

这对于解决方案很重要:

  • 不要求开发人员永久遵守额外的“其他#错误”规则。我们都很懒...
  • 运行时成本为0
  • 支持布尔运算符“ #if(feature_1 || feature_2)&&!feature_3”
  • “功能标记系统”的精度:开发新功能时,可以修改功能签名,将成员添加到类中,更改变量的类型,添加新类。 ..从字面上看,它等同于在同一个文件中包含两个分支(主分支和要素分支)。通过打开和关闭标志从一个切换到另一个。任何代码修改都可以带有功能标记!

我对可能的模板和/或面向宏的解决方案很好奇。

根据评论问题进行编辑:

使用C99的简单解决方案会很好。目前,我们的软件使用Cpp11编译器进行编译。但是,即使Cpp17解决方案也适合以后使用……任何解决方案都是好的,向后兼容越多越好(因为更多的人可以使用它!)。

2 个答案:

答案 0 :(得分:11)

希望我完全理解这些要求。如果没有,请告诉我,我将编辑或撤消此答案。

以下代码(C ++ 11)符合以下要求:

  • “不要求开发人员永久遵守额外的“其他#错误”规则。我们都很懒...”:实际上只需要使用一次(static_assert()定义允许的功能组合)。
  • “运行时成本为0”:是的,这要感谢编译器的优化器(如果启用)。
  • “要支持布尔运算“ #if(feature_1 || feature_2)&&!feature_3””:是的,当然,但不使用预处理指令
  • “功能标记系统”的精度[...请参见OP的问题和评论]”:不完全是。这里没有条件编译,因此始终会编译整个代码,并且无论功能组合如何,都必须定义运行时代码中使用的所有类型(即使它们是不同的方式)。但是,编译器的优化器会清除未使用的代码(如果已打开)。

这就是说,这种解决方案使代码复杂化。以下内容在软件的某些精确部分中很有用,但我不会用它来处理代码的整个条件激活。我通常将这些内容与普通分支和预处理器指令结合使用。因此,请把下面的代码作为“小极端主义例子”。

#include <iostream>

// Having all your flags encapsulated in a namespace or in a class allows you to avoid errors tied to typos:
// - "#if feaature_1" (notice the typo in 'feaature') would just exclude some code silentely
// - but "if (FeatureFlags::feaature_1)" (same typo) produces a compile error, which is better
class FeatureFlags
{
public:
    static constexpr bool feature_1 = false; // This would also work with 'const' instead of 'constexpr' actually.
    static constexpr bool feature_2 = true;
    static constexpr bool feature_3 = true;
};


// We want to define a conditional class Foo. But we can't just use FeatureFlags to do conditional compile, and 
// we can't test FeatureFlags with preprocessor #directives either. So we split it as follow:
// - There's one version of it just for FeatureFlags::feature_1
// - There's another for FeatureFlags::feature_3 provided FeatureFlags::feature_1 is not defined
// - And there's a default one that deliberately cause a compile time error as we want
//   either FeatureFlags::feature_1 or FeatureFlags::feature_3 to be activated, in this example.

// This pure virtual class is just there to cause compile-time errors should we forget to
// implement a part of the class's behaviour in our Foo variants. 
// This is not mandatory: if we don't use such an interface we'll just have compile-time errors later 
// in the run-time code instead of having them at class definition level.
// This doesn't cause performances issues as the compiler's optimizer will handle that for us, we'll see later.
class Foo_Interface
{
public:
    virtual ~Foo_Interface() 
    {}

    virtual void doSomething() = 0;
};


// Will be stripped out by modern compilers' optimizers if FeatureFlags::feature_1 is false
// Side note: Methods are implemented inline just to have a compact example to copy/paste. 
// It would be best to have them in a separate .cpp file of course, as we usually do.
class Foo_Feature1 : public Foo_Interface
{
public:
    Foo_Feature1()
        : i(5)
    {}

    virtual ~Foo_Feature1() 
    {}

    virtual void doSomething()
    {
        std::cout << "Foo_Feature1::doSomething() with " << i << std::endl;
    }

private:
    int i;
};


// Will be stripped out by modern compilers' optimizers if FeatureFlags::feature_1 is true or FeatureFlags::feature_3 is false
class Foo_NotFeature1But3 : public Foo_Interface
{
public:
    Foo_NotFeature1But3()
        : d(1e-5)
    {}

    virtual ~Foo_NotFeature1But3() 
    {}

    virtual void doSomething()
    {
        std::cout << "Foo_NotFeature1But3::doSomething() with " << d << std::endl;
    }

private:
    double d;
};


// Will be stripped out by modern compilers' optimizers if FeatureFlags::feature_1 is true or FeatureFlags::feature_3 is true
class Foo_Default : public Foo_Interface
{
public:
    Foo_Default()
    {
        // This produces an error at compile time should the activated features be unconsistant.
        // static_assert(cdt,msg) can be used everywhere, not only in blocks. It could have been right under 
        // the definition of FeatureFlags for example. It really depends on where you would like the error to appear.
        static_assert(FeatureFlags::feature_1 || FeatureFlags::feature_3, "We shouldn't be using Foo_Default, please enable at least feature 1 or 3");
    }

    virtual ~Foo_Default()
    {}

    virtual void doSomething()
    {}
};


// Now we can conditionnaly define Foo:
// - Foo is Foo_Feature1 if FeatureFlags::feature_1 is true. 
// - Otherwise, it is either Foo_NotFeature1But3 or Foo_Default depending on FeatureFlags::feature_3
typedef std::conditional
<
    FeatureFlags::feature_1,
    Foo_Feature1,
    std::conditional<FeatureFlags::feature_3, Foo_NotFeature1But3, Foo_Default>::type
>::type Foo;


void main()
{
    // What follows is automatically inlined in release mode, no virtual table. Not even an object.
    // If Foo becomes bigger or more complicated, this might change. But in that case this means the
    // cost of the vtable becomes neglictible. All of this can perfectly be done with no inheritance at 
    // all though (see comments at Foo_Interface's definition)
    Foo f;

    f.doSomething();


    if (FeatureFlags::feature_1)
    {
        // Do something or not depending on feature_1.
    }

    if (FeatureFlags::feature_2)
    {
        // Do something or not depending on feature_2.
    }

    if ((FeatureFlags::feature_1 || FeatureFlags::feature_2) && !FeatureFlags::feature_3)
    {
        // Why not, after all, but that sounds odd...
    }
}

答案 1 :(得分:4)

如果对于不存在的标志的缺少诊断是您使用预处理器方法的唯一问题,则也可以只使用类似函数的宏:

#define feature_flag() 0

int main()
{
#if feature_flag()
    printf("A");
#else
    printf("B");
#endif
}

如果该标志不存在,则会触发诊断,否则会像普通宏一样起作用。