SoFunction
Updated on 2025-04-13

If-constexpr syntax and functions in C++

1 if-constexpr syntax

1.1 Basic syntax

​ if-constexpr syntax is a new syntax feature introduced in C++ 17, also known as constant if expressions or static if (static if). The purpose of introducing this language feature is to further expand the ability of C++ to calculate and evaluate during the compilation period, and to more conveniently implement branch selection actions during the compilation period. Early C++ lacked similar language features, so C++ developers had to use idiomatic techniques such as tag dispatching or use template specialization mechanism to enable the compiler to "turningly" realize some static choices when deriving template parameters. The C++11 standard clarifies the application of the SFINAE mechanism and providesenable_if, the combination of the two can more easily realize branch selection during the compile period, but compared with if-constexpr, the readability and ease of use are several blocks worse.

​ Before introducing the "magic powers" of if-constexpr, let's take a look at the syntax form of if-constexpr. In fact, when we introduce how to support structured bindings in our design types ("Let custom types support structured bindings"), we will implement them.get<N>()This syntax is used for member methods. Here I will review the expression form of the if-constexpr syntax:

struct FooTest
{
    template&lt;std::size_t N&gt;
    const auto&amp; get() const
    {
        if constexpr (N == 0) return name;
        else if constexpr (N == 1) return age;  //else if the constexpr specifier in the branch can be omitted        else return weight;
    }
};

The judgment of the template parameter N in this function is performed during compilation, and which branch returns data is also determined during compilation. The code in the false branch will not even appear in the instantiated function code. byget<N>()Member functions are used as an example. If the code using FooTest is usedget<0>()Take the name member, and after final instantiation, you will getget<0>()Specialized implementation:

struct FooTest
{
    template<>
    const std::string& get<0>() const
    {
        return name;
    }
};

If the code is still usedget<1>()Take the value of age, and the compiler will also instantiate itget<1>()This specialized version:

template<>
const int& get<1>() const
{
    return age;
}

1.2 Extended Description

1.2.1 Conditional expressions

​ Constant conditional expressions in the if expression must be a constant expression of type bool, or a constant expression that can be converted to a constant expression of type bool. Because this conditional expression is evaluated during the compilation period, constexpr modified constants or functions can appear in this expression, but variables or non-constexpr functions during runtime cannot appear in conditional expressions.

In C++17, a lambda expression (lambda function) can be explicitly declared as constexpr, but when a lambda expression and related context achieve a closure together, if the closure is used in a constexpr context, it will be used as constexpr even if it is not explicitly declared as constexpr, for example:

auto DoubleIt = [](int n)  {  return n + n;  };
template<std::size_t N>
bool Func2()
{
    if constexpr (DoubleIt(N) < 100)
        return true;
    else
        return false;
}
std::cout << Func2<10>() << ", " << Func2<50>() << std::endl;  //1, 0

1.2.2 false branch processing

When the function template is instantiated, the statement evaluated as a false branch will not appear in the final instantiated function code, but the compiler will syntax check it and will also report an error when there is a syntax error (some compilers report an error). Although syntax checks are performed, the return statement in the false branch will not participate in the function's return value type presumption, such as this example:

template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; 
    else
        return t; 
}

whenstd::is_pointer_v<T>When true, the return t statement in the else branch does not participate in the return value type assumption, and the function return value type assumption*ttype. whenstd::is_pointer_v<T>When false is exactly the opposite, the return t statement in the else branch will determine the return value type, and the function return value type is assumed to be the type of t. It should be noted here that if you cancel the else branch of if-constexpr and use the commonly used default return method for function design, the compiler will report an error, such as:

template<typename T>
auto get_value(T t)
{
    if constexpr (std::is_pointer_v<T>)
        return *t; 
    return t; 
}

The code semantics of the get_value2() function are the same as the previous get_value() function, but the compiler will report an error because the last return statement also participates in the assumption of the return value type, and when T is a pointer type, the assumption of the return value type of the two return statements will contradict each other.

Although the compiler will syntax check the code of the false branch, the code of the false branch will be discarded, so it does not participate in the code link. For example, this example:

extern int x; //
int f()
{
    if constexpr (true)
        return 0;
    else if (x)
        return x;
    else
        return -x;
}
f(); //Call f

Although the global variable x has only one extern declaration and is not defined, this code compiles normally without errors. Because the code of the else if and else branch is discarded, the compiler does not need to locate x for the link code.

The reason why the compiler syntax checks the false branch code that is ready to be discarded is that it analyzes the entire if statement and understands the start and end positions of each branch logic, so that the code of the true branch can be properly retained and the code of the false branch is discarded.

1.2.3 Initialization statement

​ C++ 17 has begun to support the use of initialization statements in if statements. Of course, if-constexpr syntax also supports initialization statements, but it requires that initialization statements can only use constants and constant expressions. For example, the initialization of k in this example:

template<std::size_t N>
bool Func()
{
    if constexpr (constexpr std::size_t k = 3; (N % k) == 0)
        return true;
    else
        return false;
}

k must be a constant, and the expression that initializes k must be a constant expression.

2 The role of if-constexpr

​ If-constexpr, which can be executed during the compilation period, is used for conditional judgment in template metaprogramming. It not only expands the branch processing capabilities of template metaprogramming, but also simplifies a lot of branch judgment codes that were previously implemented in very complex ways, making template metaprogramming's processing code for conditional branches more intuitive and easier to understand. In this part, we use three examples to introduce the role of if-constexpr, including a comparison of traditional tag dispatching and template specialization idioms.

2.1 Simplify the processing of variable parameters

​ Use if-constexpr to improve the readability of generic code, as introduced in the previous sectionget<N>()If you do not use if-constexpr, you need to use recursive derivation of the template to solve the problem of uncertain number of N. The specific approach is to define a generalized version plus a specialized version. This not only makes you troublesome to implement, but also greatly reduces the readability of the code. In this section, we use a function template for summing that we introduced before as an example to introduce the benefits of if-constexpr's processing of variable parameters and improving the readable performance of code.

Before C++17, there were no folding expressions and if-constexpr. The processing of parameter packages required recursive derivation of templates, and it was necessary to define a specialized instance that ended the recursive derivation, which was very unintuitive:

template&lt;typename T&gt;
auto Sum(T arg)
{
    return arg;
}
template&lt;typename T, typename... Args&gt;
auto Sum(T arg, Args... args_left)
{
    return arg + Sum(args_left...);
}
std::cout &lt;&lt; Sum(3, 5, 8) &lt;&lt; std::endl;  //Output 16std::cout &lt;&lt; Sum(std::string("Emma "), std::string(" love cats!")) &lt;&lt; std::endl; //Output Emma love cats!

C++ 17 introduces fold expressions, and it is much easier to use fold expressions. Let’s take a look at the version of the fold expression:

template<typename... Args>
auto Sum(Args&&... args)
{
    return (0 + ... + args);
}

However, the syntax of folding expressions makes many beginners "creepy". If it is changed to implementing it with if-constexpr, the code will be more intuitive and readable:

template <typename T, typename... Args>
auto Sum(T arg, Args... args)
{
    if constexpr (0 == sizeof...(Args))
        return arg;
    else
        return arg + Sum(args...);
}

2.2 More flexible than std::enable_if

​ SFINAE (Substitution Failure Is Not An Error) means that during the template derivation process, if a meaningless error result is obtained after the template parameter replacement, the compiler does not report an error immediately, but temporarily ignores the template function declaration and continues to parameter derivation and replacement. The std::enable_if introduced in C++11 is the most direct way to implement SFINAE. The following uses the ToString() function as an example (note that this is not a rigorous implementation, just as an example), and see how to use std::enable_if to implement conditional branches during the compile period.

//You can also use enable_if_ttemplate&lt;typename T&gt;
std::enable_if&lt;std::is_arithmetic&lt;T&gt;::value, std::string&gt;::type
ToString(T t)
{
    return std::to_string(t);
}
template&lt;typename T&gt;
std::enable_if&lt;!std::is_arithmetic&lt;T&gt;::value, std::string&gt;::type
ToString(T t)
{
    return t;
}

std::to_string() supports converting an integer or floating-point numerical data into a string. If T is of type std::string, no conversion is required. The function of std::enable_if is to control the return value type, so that an incorrect function declaration is generated when type T does not match the function code (for example, the to_string() function does not support the std::string type). For example, when T is type std::string (not a numeric type), the compiler substitutes the two template functions and gets two function declarations:

template<>
ToString<std::string>(std::string t);
template<>
std::string ToString<std::string>(std::string t);

The first replacement result has no function return value, and is a syntactically wrong function declaration. The compiler will discard this replacement result and select the second syntactically correct one as the final ToString() function overload the adjudication result. In this way, the purpose of branch selection during the compilation period is achieved through the cooperation of std::enable_if and SFINAE mechanism.

​ However, when using std::enable_if to control, std::enable_if can only divide the conditions into two situations, that is, the two conditions must be mutually exclusive, that is, one is a true condition and the other must be a false condition. Otherwise, once the two judgment conditions are true, two correct results will occur, causing the compiler to report a "ambiguous function call" compilation error. From the above example, we can see that although std::enable_if can also implement conditional branch selection during the compilation period, the code is not intuitive, with many constraints, and can only implement the selection of two branches. Now take a look at the implementation using if-constexpr:

template<typename T>
auto ToString(T t)
{
    if constexpr (std::is_arithmetic<T>::value)
        return std::to_string(t);
    else
        return t;
}

Such code is more intuitive and easier to understand and maintain than writing two overloaded functions to allow the compiler to match calls according to SFINAE principles.

2.3 More intuitive than tag dispatching

​ tags are some empty types without data and operations. They can be used as function parameters to affect the compiler's choice of overloaded functions. Using tag dispatching technology, we must first define tags. According to the example in this article, we define two tags:

struct NumTag {};
struct StrTag {};

Next, we need to define the overloaded function. The only different parameter is the tag type. The tag type as a dumb parameter of the function only affects the compiler's choice of the overloaded function. In the end, this parameter that does not exist will be optimized by the compiler:

template <typename T>
auto ToString_impl(T t, NumTag)
{
    return std::to_string(t);
}
template <typename T>
auto ToString_impl(T t, StrTag)
{
    return t;
}

Finally, implement ToString(), and determine whether to call ToString_impl(t, NumTag()); or ToString_impl(t, StrTag()); and in detail, the Eight Immortals cross the sea, each showing their magical powers, such as this method of using custom type_traits:

template &lt;typename T&gt; //A generalized version that is not rigorousstruct traits
{
    typedef NumTag tag;
};
template &lt;&gt;   //Specialized version for std::stringstruct traits&lt;std::string&gt;
{
    typedef StrTag tag;
};
template &lt;typename T&gt;
auto ToString(T t)
{
    return ToString_impl(t, typename traits&lt;T&gt;::tag());  //Select ToString_impl() according to traits<T>::tag}

​ Comparing the version implemented with if constexpr in the previous section, it can be seen that the code using tag dispatching is obscure. You need to study the definition of tag to understand the specific conditions for branch selection. The code implementation is not as intuitive as if constexpr.

3 Difference between if-constexpr and if

3.1 if why not

​ If the example of the ToString() function in the previous section does not use if-constexpr, just use if to implement it directly like this:

template<typename T>
auto ToString(T t)
{
    if (std::is_arithmetic<T>::value)
        return std::to_string(t);
    else
        return t;
}

Is that OK? The answer is no, becausestd::is_arithmetic<T>It is evaluated during the compilation period. When the code needs to convert the integer 42 into a string and call ToString(42), the incoming parameter is int or double, and the evaluation result is true. At this time, the function template is instantiated as:

auto ToString(int t)
{
    if (true)
        return std::to_string(t);
    else
        return t;
}

This instantiation result cannot be compiled because is the return value an integer or std::string? The return value types of the two return statements are inconsistent. Then see that the incoming parameter is std::string. At this time, the evaluation result of if is false, and the function template is instantiated as:

auto ToString(std::string t)
{
    if (false)
        return std::to_string(t);
    else
        return t;
}

Although there is no problem to go to the else branch and return t directly, there will be problems with the compilation of the if branch, because std::to_string() does not support the std::string type. Therefore, it is not possible to use the if statement directly.

3.2 Why can if-constexpr

Now compare the situation of using if-constexpr. As mentioned earlier, only syntax analysis is performed for false branch compilation and no code is generated. So when the ToString(42) call appears in the code, the incoming parameter is int and the evaluation result is true. At this time, the function template is instantiated as:

std::string ToString(int t)
{
    return std::to_string(t);
}

When the incoming parameter is of string type, the else branch becomes true branch and is retained. The result of instantiation of the function template is:

std::string ToString(std::string t)
{
    return t;
}

The final instantiation result is the same as the result of using std::enable_if , but the syntax is simpler and intuitive than std::enable_if .

4 Differences between if-constexpr and #if

​ Compilation period if expressions can easily remind people of the C++ conditional compilation directive #if, but their differences are still very obvious, with three main points:

  • The processing stages are different: #if conditional compilation directive is parsed in the code preprocessing stage. When the preprocessor process is completed and submitted to the compiler, the compiler can only see the content of the true branch, while the code of if-constexpr is processed in the compilation stage;
  • Conditional expression requirements are different: first of all, the stages of code processing are different. #if can only use macros for definition, macros predefined by the compiler, and environment variables, and cannot use functions or variables in the code. The conditional expression of if-constexpr can be a constant in the code, or a constant function;
  • The false branches are handled differently: the false branch compiler does not perform syntax checks in conditional compilation. In fact, they are filtered out during the precompilation stage, and the false branch in if-constexpr also performs syntax checks.

This is all about this article about if-constexpr in C++. For more related C++ if-constexpr content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!