C++

C++ - Constness

constexpr, if constexpr

Posted by Rico's Nerd Cluster on February 1, 2023

constexpr

constexpr in C++ allows expressions to be evaluated at compile time rather than runtime. This feature helps optimize programs by computing constant values during compilation and enables creating objects with immutable values that the compiler can use in constant expressions. Using constexpr with functions and variables encourages safer and more efficient code by catching errors early and reducing runtime overhead.

Compared to const:

  • const marks data as read-only after initialziation, but its value is not necessarily known during compile time.
  • constexpr provides a stronger compile-time guarantee.
    • constexpr variables are always const
    • constexpr functions and pointers doesn’t make everything const
  • Use constexpr in compile-time evaluations:
    • static_asserts
    • template parameters

constexpr Functions

A constexpr function is NOT a const function, nor does it imply that its parameters or local const are const. It merely guarantees that when provided with constexpr arguments, the function can be evaluated at compile time.

One example is: without constexpr function, static_assert would throw an error:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

// A constexpr function to compute factorial of n
int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    // Compile-time evaluation using static_assert, but this would throw an error
    static_assert(factorial(5) == 120, "factorial(5) should be 120");
    return 0;
}

With constexpr, static_assert is happy. The function can be evaluated during runtime too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

// A constexpr function to compute factorial of n
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    // Compile-time evaluation using static_assert
    static_assert(factorial(5) == 120, "factorial(5) should be 120");
    // Runtime usage: even though the function is constexpr,
    // it can be used with values not known until runtime.
    int runtime_value = 6;
    int runtime_result = factorial(runtime_value);
    std::cout << "Factorial of " << runtime_value << " is " << runtime_result << "\n";
    return 0;
}

if constexpr [C++ 17] To branch to different code path in compile time meta programming

If you have a code branch to be discarded in compile time, and the condition is available during compile time, if constexpr could help reduce the unecessary code branch.

1
2
3
4
5
6
7
inline size_t hash_function(const NNPoint &pt) {
    if constexpr(dim ==2) {
        return size_t(((pt[0] * 73856093) ^ (pt[1] * 471943)) % 10000000)
    } else if constexpr(dim == 3){
        return size_t(((pt[0] * 73856093)^ (pt[1] * 471943) ^ (pt[2] * 83492791)) % 10000000); 
    }
}

if Constexpr & static_assert

On some C++ 20 comiliers (for which I don’t know further specifications yet), in if constexpr, if static_assert() is not dependent on any template parameter, it would fire anyways. So one needs to make it explicitly dependent on the desired template parameter so it’s reached in the correct code path.

1
2
3
4
5
6
7
8
9
  if constexpr (dim == 3)
      query_pt = {pt.x, pt.y, pt.z};
  else if constexpr (dim == 2)
      query_pt = {pt.x, pt.y};
  else
      // BAD: 
      // static_assert(false, "dimension can only be 2 or 3");
      // GOOD: 
      static_assert(dim != 2 && dim != 3, "dimension can only be 2 or 3");

However, on this C++ compiler, below works fine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

template<bool true_val>
void g() {
    if constexpr (true_val) {
        // []<bool flag = false>() { static_assert(flag, "static assert"); }();
        // This works properly
        static_assert(true_val, "static assert true val");
        static_assert(false, "static assert false in constexpr true path");
    } else {
    }
}

int main()
{
    g<false>();
    return 0;
}

Constexpr Improvements [C++20]

constexpr rules in cpp20 are relaxed. Operations like allocation, resizing, element access on vectors are allowed, as long as those operations themselves meet the requirements for constexpr evaluation

While vectors still can’t be compiled on my C++20 compiler, one can use std::array

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <numeric>
#include <array>


int main() {

    std::cout << std::endl;

    constexpr std::array myArray{1, 2, 3, 4, 5};                                     // (1)
    constexpr auto sum = std::accumulate(myArray.begin(), myArray.end(), 0);         // (2)
}