Флаговые флаги

Цель состоит в том, чтобы иметь систему флагов Feature без затрат времени исполнения. Простое решение 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 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;
}

Вопрос:

Есть ли элегантный способ гарантировать, что использование флага функции несуществующего (неправильно написанного) экземпляра приводит к ошибке компиляции?

Это важно для решения:

  • Не требовать постоянной разработки дополнительного "else #error" от разработчика. Мы все ленивы...
  • Чтобы иметь 0 временных затрат
  • Для поддержки логических операций "#if (feature_1 || feature_2) &&! Feature_3"
  • Точность "Feature Flag System": при разработке новой функции вы можете изменить подпись функции, добавить член в класс, изменить тип переменной, добавить новый класс... Это буквально эквивалентно наличию двух ветвей (главная и особенность), живущих в одном файле. Переход от одного к другому путем включения и выключения флага. Любая модификация кода может быть отмечена функцией!

Мне очень интересно узнать о возможном шаблоне и/или макро-ориентированном решении.

Изменить комментарий:

Простое решение с C99 было бы неплохо. В настоящее время наше программное обеспечение компилируется с помощью компилятора Cpp11. Но даже решение Cpp17 было бы неплохо для более позднего... любое решение хорошо, тем лучше обратная совместимость лучше (поскольку все больше людей могут ее использовать!).

Ответы

Ответ 1

Надеюсь, я полностью понял требования. Если нет, сообщите мне, и я отредактирую или отозму этот ответ.

Код belows (С++ 11) соответствует следующим требованиям:

  • "Не требовать постоянной разработки дополнительного" else #error "от разработчика. Мы все ленивы...": на самом деле это просто нужно один раз (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 one version of it just for FeatureFlags::feature_1
// - There another for FeatureFlags::feature_3 provided FeatureFlags::feature_1 is not defined
// - And there 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 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 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 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...
    }
}

Ответ 2

Если недостающая диагностика для несуществующих флагов является вашей единственной проблемой при использовании препроцессора, вы также можете просто использовать макросы, подобные функции:

#define feature_flag() 0

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

Это приведет к диагностике, если флаг не существует, но ведет себя так же, как и обычные макросы.