Skip to content

Cascaded Configuration Sets for C++1y

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;
    }
    
  • Author of the class specializes its default configuration with necessary options (constants, types, static methods, etc.)
  • 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*);
    	};
    }
    
  • Author may define additional configuration sets, from scratch or cascaded from already defined ones
  • namespace configuration {
    	template<>
    	struct Configuration<MyClass, Debug> : /* Debug cascaded from  Default */
    		Configuration<MyClass, Default> {
    		static constexpr int MyValue = 1;
    	};
    }
    
  • Author uses defined options as needed
  • class MyClass {
    public:
    	using config = configuration::Configuration<MyClass>;
    	using MyType = config::MyType;
    	static constexpr int MyValue    = config::MyValue;
    	static constexpr bool MyOption  = config::MyOption;
    };
    
  • User of the class specializes configuration selector with the identifier of active configuration
  • namespace configuration {
    template<>
    struct Selector<MyClass> {
    	typedef Debug type; /* configuration set selector */
    };
    }
    
  • If needed, user may define its own configuration identifier and configuration set
  • 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:

  1. Module's configuration definitions are placed in header files with different extension (such as *.ccs)
  2. ccs headers must be self-sufficient, e.g. they should include core template definitions #include <ccs> and other headers they depend on
  3. User's configuration is defined in (or is included via) "configuration.h" header
  4. "configuration.h" includes *.ccs files for all modules needed in the user's project
  5. "configuration.h" specializes configuration selectors as needed
  6. Every header that defines a configurable class must include "configuration.h" header
  7. However, no assumption should be made about content of "configuration.h", compilation must succeed even it is empty
  8. Normal module headers include "configuration.h" first and then module's *.ccs header
  9. #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

Your email is never published nor shared. Required fields are marked *
*
*

*