Introduction
Preprocessor macros are widely used in C++ for configuring reusable modules, targeting different platforms or working around compiler’s differences. Such approach is inherited from C and used even in modern pure C++ libraries, such as boost.
The aim of this work is to research on suitability of C++ templates and language features for establishing and maintaining compile time configuration of run-time values without use of the preprocessor. Although there are many disputes on advantages and disadvantages of templates vs macros, author’s belief is templates are better in many cases just because they are the first class citizens of C++ language.
Goals
The following features of a new configuration facility for C++ projects are identified as desirable:
- Configuration symbols are organized in namespaces
- Configuration options are grouped by the class in which they are used
- Configuration options are grouped into sets for targeting different environments or use cases (Debug, Release, ARM, MIPS, etc.)
- Configuration sets are cascaded - e.g. a set may reuse all options defined in another set and override them as necessary
- Active configuration is easily switchable in the user's project
- Use of configuration imposes no run-time penalty
- Design encourage writing more readable code
Design of Cascaded Configuration Sets
The following design is suggested for implementing Cascaded Configuration Sets
- Main configuration template is parametrized with user class and a configuration set identifier
- Configuration set identifier is an incomplete class declared just for these needs
- Default configuration set is selected by configuration selector
namespace configuration { /* predefined configuration set identifiers */ class Default; class Release; class Debug; /* selector of active configuration set */ template<class UserClass> struct Selector { /* default configuration set */ typedef Default type; }; /* Declaration of main configuration template */ template<class UserClass, typename = typename Selector<UserClass>::type> struct Configuration; }
namespace configuration { template<> struct Configuration<MyClass, Default> { static constexpr int MyValue = 300; static constexpr bool MyOption = true; typedef signed short MyType; static bool myFunc(const char*,const char*); }; }
namespace configuration { template<> struct Configuration<MyClass, Debug> : /* Debug cascaded from Default */ Configuration<MyClass, Default> { static constexpr int MyValue = 1; }; }
class MyClass { public: using config = configuration::Configuration<MyClass>; using MyType = config::MyType; static constexpr int MyValue = config::MyValue; static constexpr bool MyOption = config::MyOption; };
namespace configuration { template<> struct Selector<MyClass> { typedef Debug type; /* configuration set selector */ }; }
class AltConfig; /* ID of an alternate configuration */ namespace configuration { typedef AltConfig ActiveConfig; /* configuration set selector */ //typedef Debug ActiveConfig; template<> struct Selector<MyClass> { typedef ActiveConfig type; }; template<> struct Configuration<MyClass, AltConfig> : Configuration<MyClass, Release> { static constexpr int MyValue = 400; }; }
Implementation
Design described in the previous chapter has one problem - configuration specialization must precede it use. E.g. template<> struct Selector
must appear to the compiler before using config = configuration::Configuration
. This requires inclusion of user's headers in between the libraries headers. Similar practice is commonly used by authors of libraries that needs user's configuration with preprocessor macros. To use such practice in this design, configuration set definitions, configuration set instantiation and use of configuration must be kept in different headers. E.g. instead of single *.h or *.hpp file per module there should at least two. To make this practice consistent across all adopters of Cascaded Configuration Sets the following guidelines for structuring header files are recommended:
- Module's configuration definitions are placed in header files with different extension (such as *.ccs)
- ccs headers must be self-sufficient, e.g. they should include core template definitions
#include <ccs>
and other headers they depend on - User's configuration is defined in (or is included via)
"configuration.h"
header "configuration.h"
includes *.ccs files for all modules needed in the user's project"configuration.h"
specializes configuration selectors as needed- Every header that defines a configurable class must include
"configuration.h"
header - However, no assumption should be made about content of
"configuration.h"
, compilation must succeed even it is empty - Normal module headers include
"configuration.h"
first and then module's *.ccs header #pragma once
should be used to avoid multiple inclusion of *.ccs and"configuration.h"
headers
Examples for this article are available on GitHubGist
Post a Comment