My latest little experiment relates to compile-time options and eliminating preprocessor checks in user code. I’m not a big fan of MACROs, especially when they are simply used to make compile-time branches. I am also not a fan of other techniques used to minimize this problem. With C++17, we now have a beautiful and simple tool that can help remove all these preprocessor checks, if constexpr.

This is a great example where we can “make simple tasks simple”. I don’t know if the standard members had this sort of use in mind for if constexpr, but I sure like it. The most important gain for me is auto-completion support. With this technique, you can easily inquire what compile-time options are available to you using auto-complete in your IDE. Auto-completion is great, and anything that helps your IDE auto-complete things is also great 🙂

For this example to work, you’ll need the latest Visual Studio 2017 15.5 preview (for inline variables, VS2017 supports if constexpr). It will work on AppleClang out of the box. The examples use CMake profusely as well.

The Benefits

  • Reduce preprocessor directives throughout your code, gather them in a single file.
  • Easy to read and easy to write compile-time conditionals that have no impact on performance.
  • Auto-complete friendly.
  • Type-safe and all that good stuff.
  • Non-typed template parameter fun, oh yeah.

A Quick Look at the Result

Using the proposed technique, your user code will look something like this.


int main(int, char**) {
	if (runtime_options.a_launch_option) {
		std::cout << "Do runtime dependent things." << std::endl;
	}

	if constexpr (build_options.my_bool) {
		std::cout << "Do compile-time dependent things." << std::endl;
	}

	return 0;
}

Interested? Lets dive in.

CMake Setup

In this example, we will be setting our compile-time options with CMake. There is nothing stopping you from using built-in preprocessor definitions or other tools as well. I will be creating as simple a setup as possible. Obviously, a production code-base would be much more complex.

With CMake, you can use a configuration header as input, and it will generate a header file with your compile-time options. Here is an example of a simple config header file.

build_options.h.in


#pragma once

#cmakedefine MY_INT @MY_INT@
#cmakedefine01 MY_BOOL

CMake will replace the #cmakedefines with definitions set either in your CMakeLists file or coming from the command-line. The #cmakedefine01 directive tells CMake to use 0 or 1 for boolean values (instead of ON/OFF for example).

CMakeLists.txt


cmake_minimum_required(VERSION 3.8.0)
project(build_options CXX)

# Simple options.
set(MY_INT 42)
option(MY_BOOL "A boolean." On) # Use cmake .. -DMY_BOOL=Off to turn off.

# This command parses and outputs the header.
configure_file("src/build_options.h.in"
	"include/build_options.h")

# General example things.
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.c" "src/*.hpp" "src/*.h")
add_executable(${PROJECT_NAME} ${SOURCES})

# Add generated header path to include paths.
target_include_directories(${PROJECT_NAME} PRIVATE
	"${CMAKE_CURRENT_BINARY_DIR}/include")

if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
	# CMAKE_CXX_STANDARD for MSVC should be fixed in CMake 3.10.
	target_compile_options(${PROJECT_NAME} PRIVATE /std:c++latest)
endif()

Here we create a very basic CMake config file. We pass our build_options.h.in file to the appropriate configure_file command. It will output the processed build_options.h in an include directory inside the build directory. Remember to add this directory to your include path.

Looking at the generated build_options.h, we now read :

build_options.h


#define MY_INT 42
#define MY_BOOL 1

Perfect! Now lets add the magic sauce for the if constexpr support.

build_options.h.in


#pragma once

#cmakedefine MY_INT @MY_INT@
#cmakedefine01 MY_BOOL

struct BuildOptions {
	constexpr static int my_int = MY_INT;
	constexpr static bool my_bool = static_cast<bool>(MY_BOOL);
};
inline constexpr BuildOptions build_options;

We define a BuildOptions struct with constexpr static members. You could use a namespace with global variables for a similar result. Here, I want the dot notation for the options, so I also declare an inline constexpr object. Obviously you can architecture the hell out of this, but my goal is simply to demonstrate the concept (no, not those concepts).

And that is pretty much it for the simple example. Here’s how you can use it.

main.cpp


#include "runtime_options.h"
#include <build_options.h>

#include <iostream>

int main(int, char**) {
	runtime_options.a_launch_option = true;

	if (runtime_options.a_launch_option) {
		std::cout << "Do runtime things." << std::endl;
	}

	if constexpr (build_options.my_bool) {
		std::cout << "Bool is set." << std::endl;

		if constexpr (build_options.my_int == 42) {
			std::cout << "  Do something with int." << std::endl;
		}
	}

	return 0;
}

I’ve added a runtime equivalent to show how easy it is to read both side-by-side. In the example, the runtime options are quite basic. In production, these might come from command-line options or de-serilializing some configuration ini/json/yaml file. You’d probably want const members and a way to set this all up.

runtime_options.h


#pragma once

// WARNING: High octane engineering follows!
struct RuntimeOptions {
	bool a_launch_option = false;
};
inline RuntimeOptions runtime_options;

 

Pushing Things a Little Further

Now this is all great, and very readable, but we can do so much more. I’ll use a fantasy use-case to demonstrate some more complex options. This next example won’t truly work, as you need C++17 to compile the code. But I found the idea perfect to quickly demonstrate how you can take things further. It may become relevant in the future.

Lets detect the latest C++ standard supported by our compiler. CMake provides a list of supported C++ features through the CMAKE_CXX_COMPILE_FEATURES variable. We can inquire about what standard our compiler supports.

CMakeLists.txt


# ...

# Rudimentary c++ version check. CMake supports much finer grained feature inquiry.
set(CPP_VERSION 98)
if ("cxx_std_11" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 11)
endif()
if ("cxx_std_14" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 14)
endif()
if ("cxx_std_17" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 17)
endif()

message("Your compiler supports : cpp${CPP_VERSION}")
set(CMAKE_CXX_STANDARD ${CPP_VERSION})

# ...

Once the build is run, CPP_VERSION will be set to either 98, 11, 14 or 17. CMake will also use the latest available version to compile the example.

If your thinking about enum classes right about now, your quite on target. Lets update our header input file.

build_options.h.in


/* ... */

#cmakedefine CPP_VERSION @CPP_VERSION@
enum class CppVersion { cpp98 = 98, cpp11 = 11, cpp14 = 14, cpp17 = 17 };

struct BuildOptions {
        /* ... */
	constexpr static CppVersion cpp_version = static_cast<CppVersion>(CPP_VERSION);
};

And there you have it. A CMake defined enum class you can use in non-type template parameters. Pretty exciting if you ask me! Finally, we can check it like so.

main.cpp


	if constexpr (build_options.cpp_version != CppVersion::cpp17) {
		std::cout << "Well... This wouldn't compile now would it."
				  << "Maybe this will become relevant in 20 years? ;)"
				  << std::endl;
	}

And there you have it. Simple, easy to use and read, modern compile-time options. I hope you find this useful, but most importantly, I hope it inspires you to build awesome option systems!

You can find the full example here.

Full Code

CMakeLists.txt


cmake_minimum_required(VERSION 3.8.0)
project(build_options CXX)

# Simple options.
set(MY_INT 42)
option(MY_BOOL "A boolean." On) # Use cmake .. -DMY_BOOL=Off to turn off.

# Rudimentary c++ version check. CMake supports much finer grained feature inquiry.
set(CPP_VERSION 98)
if ("cxx_std_11" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 11)
endif()
if ("cxx_std_14" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 14)
endif()
if ("cxx_std_17" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
	set(CPP_VERSION 17)
endif()

message("Your compiler supports : cpp${CPP_VERSION}")
set(CMAKE_CXX_STANDARD ${CPP_VERSION})

# This command parses and outputs the header.
configure_file("src/build_options.h.in"
	"include/build_options.h")

# General example things.
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.c" "src/*.hpp" "src/*.h")
add_executable(${PROJECT_NAME} ${SOURCES})

# Add generated header path to include paths.
target_include_directories(${PROJECT_NAME} PRIVATE
	"${CMAKE_CURRENT_BINARY_DIR}/include")

if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
	# CMAKE_CXX_STANDARD for MSVC should be fixed in CMake 3.10.
	target_compile_options(${PROJECT_NAME} PRIVATE /std:c++latest)
endif()

build_options.h.in


#pragma once

#cmakedefine MY_INT @MY_INT@
#cmakedefine01 MY_BOOL

#cmakedefine CPP_VERSION @CPP_VERSION@
enum class CppVersion { cpp98 = 98, cpp11 = 11, cpp14 = 14, cpp17 = 17 };

struct BuildOptions {
	constexpr static int my_int = MY_INT;
	constexpr static bool my_bool = static_cast<bool>(MY_BOOL);
	constexpr static CppVersion cpp_version = static_cast<CppVersion>(CPP_VERSION);
};
inline constexpr BuildOptions build_options;

runtime_options.h


#pragma once

struct RuntimeOptions {
	bool a_launch_option = false;
};
inline RuntimeOptions runtime_options;

main.cpp


#include "runtime_options.h"
#include <build_options.h>

#include <iostream>

int main(int, char**) {
	runtime_options.a_launch_option = true;

	if (runtime_options.a_launch_option) {
		std::cout << "Do runtime things." << std::endl;
	}

	if constexpr (build_options.my_bool) {
		std::cout << "Bool is set." << std::endl;

		if constexpr (build_options.my_int == 42) {
			std::cout << "  Do something with int." << std::endl;
		}
	}

	if constexpr (build_options.cpp_version != CppVersion::cpp17) {
		std::cout << "Well... This wouldn't compile now would it."
				  << "Maybe this will become relevant in 20 years? ;)"
				  << std::endl;
	}

	return 0;
}

3 thoughts on “Simplifying Compile-Time Options With if constexpr

  1. Except the `if constexpr` is completely useless here… It will be removed by the compiler anyway and the `if constexpr` allows the sub statement to be ill-formed when not activated…

    1. On the contrary, the `if constexpr` block needs to be well formed, unlike #ifdefs. “The discarded statement can odr-use a variable that is not defined” and “the discarded statement can’t be ill-formed for every possible specialization” [cppreference]. So this technique cannot fix all cross-platform problems, but is useful for some. I may write a follow with a concrete cross-platform example.

Leave a Reply