Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zero runtime costs feature flags

Tags:

c++

The goal is to have a Feature flag system with no run-time costs. A simple C99 solution is:

C99:

#include <stdio.h>

#define flag_1 1

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

    return 0;
}

not that the C++ 17 solution here seems elegant:

#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;
}

But is not working since the "if constexpr" construct is only valid where the "if" construct is. For instance this code is not valid:

if constexpr(tag_flag_1)
{
    class foo
    {

    };
}

while this one is:

#if tag_flag_1
    class foo
    {

    };
#endif

Problem with the C99 solution:

typing:

if constexpr(flag_not_exists)

will result in a compilation error, while:

#if flag_not_exists

Will not.

Of course one could always write this alternative cumbersome solution in 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;
}

Question:

Is there an elegant way to ensure that the usage of a non-existant (mis-spelled for instance) feature flag result in a compilation error?

It is important for the solution:

  • Not to require the permanent discipline of an extra "else #error" from the developer. We all are lazy...
  • To have 0 run-time costs
  • To support Boolean operations "#if (feature_1 || feature_2) && !feature_3"
  • Precision of "Feature Flag System": When you develop a new feature, you can modify function signature, add a member to a class, change the type of a variable, add a new class... It is literally equivalent to having two branches (main and feature) living in the same file. Switching from one to the other by turning flag on and off. Any code modification can be feature flagged!

I'm very curious about a possible template and/or macro oriented solution.

Edit from comment question:

A simple solution with C99 would be nice. Currently our software compiles with a Cpp11 compiler. But even a Cpp17 solution would be nice for later... any solution is good, the more backward compatible the better (as more people could use it!).

like image 693
user2346536 Avatar asked Jul 03 '18 13:07

user2346536


2 Answers

I hope I completely understood the requirements. If not, please let me know and I'll edit or withdraw this answer.

The code below (C++11) complies to the requirements the following way:

  • "Not to require the permanent discipline of an extra "else #error" from the developer. We all are lazy...": actually it is just needed once (static_assert()s that define the allowed features combinations).
  • "To have 0 run-time costs": yes, thanks to compiler's optimizer (if turned on).
  • "To support Boolean operations "#if (feature_1 || feature_2) && !feature_3"": yes, definitely, but not using preprocessing directives
  • "Precision of "Feature Flag System" [... see OP's question and comments]": not completely. There is no conditional compilation here so the whole code is always compiled and all types used in the runtime code have to be defined (even if they are a different way) regardless the features combinations. However, unused code is stripped out by the compiler's optimizer (if turned on).

This being said, this kind of solution complicates the code. The below can be useful in some precise parts of a software but I would not use it to handle the whole conditional activation of my code. I use such things in combination of plain branches and preprocessor directives usually. So please take the code below as a "little extreme example".

#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 conditionally 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...
    }
}
like image 106
Shlublu Avatar answered Nov 15 '22 12:11

Shlublu


If the missing diagnostic for non-existing flags is your only issue with the preprocessor-approach, you could also just use function-like macros instead:

#define feature_flag() 0

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

This will trigger a diagnostic if the flag doesn't exist, but behaves just like plain macros otherwise.

like image 25
Horstling Avatar answered Nov 15 '22 13:11

Horstling