Chapter 4 Preprocessor and Compilation
4.1 Conditionally compiling your source code
Use
#if
(#ifdef
,#ifndef
),#elif
,#else
and#endif
to perform conditional compilation. Note that identifies checked by#ifdef
can not only defined by#define
, but also-D
command line option when compiling.Compilers predefine some macros indicating the compiler and platform like
__GNUG__
,__amd64__
and so on.defined
operator can be used in#if
and#elif
directives:c++1
2
4.2 Using the indirection pattern for preprocessor stringification and concatenation
The stringification is to turn the argument of a function-like macro into string with
operator#
. The concatenation is to concatenate two arguments of a function-like macro together withoperator##
.The indirection pattern is to say that we’d better not just define a single function-like macro. Instead we should wrap it with another macro:
c++1
2
3
4
5
6
7// stringification
// concatenationSo why? Here is the point: arguments of function-like macro are expanded (if they are also macros) first before they are substituted into the macro body, except that they are preceded by
#
or##
, or followed by##
. Consider this example:c++1
2
3
4
5
6
std::string s3 { MAKE_STR(NUMBER) }; // s3 = "42"
std::string s4 { _MAKE_STR(NUMBER) }; // s4 = "NUMBER"For
MAKE_STR(NUMBER)
, theNUMBER
is expanded to42
first before substituted because in macroMAKE_STR
,x
is not preceded by#
or##
, or followed by##
, and then the argument for_MAKE_STR
is already42
. But for_MAKE_STR(NUMBER)
, it’s not the case.x
in_MAKE_STR
is preceded by#
so the argument won’t be expanded and the result is"NUMBER"
.
4.3 Performing compile-time assertion checks with static_assert
Use
static_assert
, which is introduced in C++11, to perform compile-time assertion check:c++1
static_assert(condition, message)
in which
condition
should be convertible to a bool value at compile time andmessage
should be a string literal.We can use
static_assert
in namespace, class and block (function) scope to check like whether the template arguments meet some requirements or whether a type has expected size:c++1
2
3
4
5
6
7
8
9
10
11
12struct alignas(8) item {
int id;
bool active;
double value;
};
static_assert(sizeof(item) == 16, "size of item must be 16 bytes");
template <typename T>
class pod_wrapper {
static_assert(std::is_pod<T>::value, "POD type expected!");
T value;
};
4.4 Conditionally compiling classes and functions with enable_if
When talking about
std::enable_if
, we need to know what SFINAE is (aha, I know). Generally speaking, when compilers encounter a function call, it will build a set of possible overloads and then select the best match, in which process the SFINAE principle takes effect. To be more precise, when an overload is found inappropriate to be a candidate (substitution failure), the compiler will just count it out of the overload set instead of throwing an error. (What really leads to an error is an empty overload set)Actually the implementation of
std::enable_if
is extremely simple:c++1
2
3
4
5
6
7template<bool Test, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> {
typedef T type;
};By contrast the usage of
std::enable_if
is a little bit verbose:c++1
2
3
4
5
6template<typename T,
typename = typename std::enable_if<std::is_integral<T>::value, T>::type
>
auto mul(T const a, T const b) {
return a * b;
}One thing worth noting is that we can use dummy template parameter to create more than one overloads with type preconditions and avoid redefinition error:
c++1
2
3
4
5
6
7
8
9
10
11
12
13
14template <typename T,
typename = typename std::enable_if<std::is_integral<T>::value, T>::type
>
auto compute(T const a, T const b) {
return a * b;
}
template <typename T,
typename = typename std::enable_if<!std::is_integral<T>::value, T>::type,
typename = void
>
auto compute(T const a, T const b) {
return a + b;
}
4.5 Selecting branches at compile time with constexpr if
C++17 introduces constexpr if
if constrexpr
, which can select branches at compile time. It can be used to simplify the code written withstd::enable_if
:c++1
2
3
4
5
6
7template <typename T>
auto compute(T const a, T const b) {
if constexpr (std::is_integral<T>::value)
return a * b;
else
return a + b;
}recall the problem dgy encountered in 2020.7.20: how to “specialize” a method of a class template
Return statements in discarded constexpr if branches won’t contribute to the deduction of return type.
4.6 Providing metadata to the compiler with attributes
- C++11 provides a standard way (instead of something like
__attribute__((...))
in gcc or__declspec()
in vc++) to provide metadata to the compiler: attributes. - Attributes are wrapped by two pairs of brackets like
[[attr]]
. They can work on almost everything: type, function, variable, or even translation unit. - Here are some useful attributes:
[[noreturn]]
indicates a function does not return;[[nodiscard]]
indicates return value of the function shouldn’t be discarded (i.e. should be assigned to a variable)[[deprecated("reason")]]
indicates that the entity is deprecated and shouldn’t be used any longer.