Skip to content

Modern C++ • Generalized constant expressions

Constant expressions are an important part of programming languages because of the opportunity for aggressive optimization in the compilation process. A constant expression is broadly defined as any expression that can be evaluated at compile time. Prior to C++11 this concept had many restrictions, with the standard explicitly defining a specific set of cases that prevented any form of generalization (and thus making the whole approach a bit cumbersome).

Typical examples of constant expressions are those used for array bounds, enumerations, switch cases and non-type template parameters. These are also called integral constant expressions and are perhaps the most widely used in C++ programming, even though the Standard defines other cases that are considered constant expressions (such as null pointers, addresses and references to non mutable data, etc.). The below code shows some examples of constant data and expressions

// Constant data
 
enum asize {SMALL=10, MEDIUM=100, BIG=1000};
const int VERY_BIG = BIG * 10;
 
struct S {
    static const int SMALL = 20;
    static const int MEDIUM = 200;
    static const int BIG = 2000;
};
 
// Array bounds require constant expressions
 
int a1[15];
int a2[SMALL];
int a3[S::MEDIUM];
int a4[VERY_BIG];
int a5[sizeof(S) * 2];
 
// Constant expressions used in flow control
 
void f(int s) {
    switch (s) {
        case S::SMALL: /*...*/ break;
        case MEDIUM:   /*...*/ break;
        case 100000:   /*...*/ break;
        default:       /*...*/ break;
    }
}
 
// Constant expressions in non-type template arguments
 
template<typename T, int S>
std::vector<T> get_buffer() {
    return std::vector<T>(S);
}
auto buf = get_buffer<int, BIG>();

Although this implementation served many use cases, there were a few issues and limitations. First of all, it was not possible to use ordinary functions or user-defined types as constant expressions, that is the following was not legal

inline int huge(int s) { return s*1000; }
 
const struct {
    int size
} s = {10};
 
int a1[huge(50)];   // error: array bound not an integer constant
int a2[s.size];     // error: array bound not an integer constant
 
void f(int s) {
    switch (s) {
        case S::SMALL: /*...*/ break;
        case huge(20): /*...*/ break;  // error: huge() not a const-expression
        case s.size:   /*...*/ break;  // error: s.size not a const-expression
        default:       /*...*/ break;
    }
}
 
auto buf = get_buffer<int, huge(10)>();  // error: huge() not a const-expression

A possible workaround would be to use macros, as follows

#define HUGE(s) ((s) * 1000)
 
int a1[HUGE(50)];
 
switch(size) {
   // ...
   case HUGE(50): /*...*/ break;
   // ...
}
 
auto buf = get_buffer<int, HUGE(50)>();

which would hinder type safety. Another limitation was the fact that it was not guaranteed that initialization of constant data occurred at compile time. In fact, despite declaring a variable as const it was possible that the variable was initialized at runtime.

struct S {
   static const int size;
};

const int limit = 2 * S::size;   // possibly initialized at run time
const int S::size = 256;

In the above case, even though S::size is initialized with a constant expression, the variable is used before the initialization occurs, so it will probably be assigned at run time. This may prevent the compiler from performing very useful optimizations on the code.

C++11 has greatly improved the mechanics of constant expressions by removing many limitations and generalizing to more constructs including regular functions and user-defined types. This is possible thanks to the introduction of the following concepts:

1. constant-expression variables
2. constant-expression functions
3. constant-expression constructors

and the new constexpr specifier.

1. constant-expression variables

constant-expression variables are simply variables declared using the constexpr specifier and that must be initialized with a constant expression, as shown in the below examples

constexpr int VERY_BIG = BIG * 10;
constexpr float pi {3.141592653589};
constexpr double c = 2.99792458e8;
constexpr double E = 8.8753 * pow(c, 2);
int k1 = 10;
constexpr int W = E + k1;    // error: not a const-expression

the constexpr here specifies that the variable is guaranteed to be evaluated at compile time. If it cannot, an error will arise. This is in contrast with the const specifier which does not provide this type of assurance, as seen above. A constexpr variable is then like a const with guaranteed compile time evaluation, which brings additional advantages in terms of optimization. In fact, if a constexpr variable is never referenced in the code it stays in the compiler tables and is not allocated. This may also be true for const variables, but there is no guarantee.

2. constant-expression functions

constant-expression functions are basically named constant expressions with parameters that return a constant value. Their body is limited to a single return statement (until C++14) and they must be declared using  constexpr and defined before use. Their evaluation at compile time requires all of its argument to be constesxpr as well. With these new features we can write the examples above as follows

constexpr int huge(int s) { return s*1000; }
 
int a1[huge(50)];   // ok: huge() is a const-expression
 
void f(int s) {
    switch (s) {
        case huge(20): /*...*/ break;  // ok: huge() is a const-expression
        default:       /*...*/ break;
    }
}
 
auto buf = get_buffer<int, huge(10)>(); // ok: huge() is a const-expression

Unlike constant-expression variables, however, constant-expression functions are more flexible in that they may be evaluated at run time as well if called with non-constant arguments. The following code shows an example of this

constexpr int allocate(int s) { return s*1000; }
 
int size; std::cin >> size;
 
int a[allocate(5)];
std::vector<int> b(allocate(size));

Here the first allocate() call is a constant expression and will be evaluated at compile time, producing a constant value, while the second call is not a constant expression and will be evaluated at run time like a regular function. One of the main advantages of constant-expression functions over the use of macros is type safety.

3. constant-expression constructors

constant-expression constructors are a natural consequence of constant-expression functions and allow for generalization of constant-expression data to user-defined types. From a conceptual point of view a constant-expression constructor is like a constant-expression function, with the technical difference that, since constructors do not return a value the evaluation of the constant expression must be carried out in the member initializer list, which must produce constants when the arguments are constant. Consider now the previous example where attempting to use a user-defined struct in constant expression failed. This can now be solved as shown in the code below

struct Sk {
    constexpr Sk(int s) : size(s) { }
    int size;
};
 
constexpr Sk s(10);   // ok: Sk's c-tor is const-expression
 
int a2[s.size];     // ok: s.size is const-expression
 
void f(int z) {
    switch (z) {
        case s.size:   /*...*/ break;  // ok
        default:       /*...*/ break;
    }
}
 
auto buf = get_buffer<int, s.size>();  //ok

The above code will now work because the user-defined type Sk can be used in constant-expression variables since it’s declared with a constexpr constructor. In fact, Sk::Sk() is evaluated at compile time and produces a constant, which can then be used wherever a constant expression is needed. Same as with constant-expression functions, constant-expression constructors are evaluated at run time if their arguments are non-constant, thus producing ordinary variables, without the need of defining multiple constructors

Sk s(10);   // ordinary variable of type Sk

The constexpr specifier can also be applied to other member methods and used to initialize constant values, as in the following example

struct Sk {
    constexpr Sk(int s) : size(s) { }
    constexpr int get_size() { return size; }
private:
    int size;
};

constexpr Sk s(10);
constexpr int big = s.get_size();  //ok: s.size() is const-expression

Neat!

const vs constexpr

One thing to keep in mind is that constexpr and const are not interchangeable, as they serve two different purposes (albeit they may exhibit the same behaviour). In particular, const‘s main purpose is that of conveying (and somewhat enforce) a “non-mutability” policy on objects, while constexpr‘s is that of generalizing the construction of constant expressions in a type-safe manner, thus allowing for more expressive and safer compile-time programming.

Published inModern C++