본문으로 건너뛰기 C++ Compile-Time Programming Complete Guide | constexpr

C++ Compile-Time Programming Complete Guide | constexpr

C++ Compile-Time Programming Complete Guide | constexpr

이 글의 핵심

Master C++ compile-time programming: constexpr, consteval, if constexpr, TMP, performance benchmarks, and practical patterns for zero-overhead runtime.

Introduction

C++ compile-time programming uses constexpr, consteval, and if constexpr to compute at compile time, eliminating runtime overhead. Analogy: Runtime computation is like cooking on order; compile-time computation is like pre-cooking and reheating—execution is instant.

What You’ll Learn

  • Understand differences between constexpr, consteval, and if constexpr
  • Distinguish compile-time vs runtime computation
  • Compare with template metaprogramming (TMP)
  • Apply practical patterns and avoid pitfalls

constexpr Basics

constexpr Variables

#include <iostream>
constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159;
int main() {
    int arr[MAX_SIZE];  // Array size must be compile-time constant
    
    std::cout << PI << std::endl;
    
    return 0;
}

Key: constexpr variables can be used as array sizes and template arguments.

constexpr Functions

#include <iostream>
// Can execute at compile time or runtime
constexpr int square(int x) {
    return x * x;
}
int main() {
    constexpr int result = square(5);  // Compile-time
    int arr[result];  // OK: compile-time constant
    
    int x = 10;
    int result2 = square(x);  // Runtime (x is not constexpr)
    
    std::cout << result << ", " << result2 << std::endl;
    
    return 0;
}

Output:

25, 100

constexpr vs const

// const: runtime constant
const int x = 10;
// constexpr: compile-time constant
constexpr int y = 10;
int arr1[x];  // ❌ Error (const not guaranteed compile-time)
int arr2[y];  // ✅ OK (constexpr is compile-time)

Key: constexpr guarantees compile-time evaluation; const only guarantees immutability.

Practical Examples

1. constexpr Functions

#include <iostream>
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
    constexpr int fact5 = factorial(5);  // 120 (compile-time)
    
    std::cout << fact5 << std::endl;
    
    return 0;
}

Output:

120

Key: Recursive constexpr functions are evaluated at compile time when arguments are constexpr.

2. constexpr Classes

#include <iostream>
class Point {
private:
    int x_, y_;
    
public:
    constexpr Point(int x, int y) : x_(x), y_(y) {}
    
    constexpr int getX() const { return x_; }
    constexpr int getY() const { return y_; }
    
    constexpr int distanceSquared() const {
        return x_ * x_ + y_ * y_;
    }
};
int main() {
    constexpr Point p(3, 4);
    constexpr int dist = p.distanceSquared();  // 25 (compile-time)
    
    int arr[dist];  // OK
    
    std::cout << dist << std::endl;
    
    return 0;
}

Output:

25

3. if constexpr (C++17)

#include <iostream>
#include <type_traits>
template<typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // Dereference if pointer
    } else {
        return t;   // Return as-is
    }
}
int main() {
    int x = 10;
    int* ptr = &x;
    
    std::cout << getValue(x) << std::endl;    // 10
    std::cout << getValue(ptr) << std::endl;  // 10
    
    return 0;
}

Output:

10
10

Key: if constexpr enables compile-time branching based on type traits, eliminating dead code paths.

4. consteval (C++20)

#include <iostream>
// Must execute at compile time only
consteval int sqr(int n) {
    return n * n;
}
int main() {
    constexpr int x = sqr(5);  // ✅ OK (compile-time)
    
    int y = 10;
    // int z = sqr(y);  // ❌ Error: runtime value
    
    std::cout << x << std::endl;
    
    return 0;
}

Output:

25

Key: consteval enforces compile-time-only execution, preventing runtime calls.

Advanced Patterns

1. Compile-Time String Hashing

#include <iostream>
constexpr unsigned int hash(const char* str) {
    unsigned int hash = 5381;
    while (*str) {
        hash = ((hash << 5) + hash) + (*str++);
    }
    return hash;
}
int main() {
    constexpr unsigned int startHash = hash("start");
    constexpr unsigned int stopHash = hash("stop");
    
    const char* command = "start";
    
    switch (hash(command)) {
        case startHash:
            std::cout << "Start" << std::endl;
            break;
        case stopHash:
            std::cout << "Stop" << std::endl;
            break;
        default:
            std::cout << "Unknown" << std::endl;
    }
    
    return 0;
}

Output:

Start

Key: Compile-time hashing enables fast string-based switch statements.

2. Compile-Time Array Generation

#include <array>
#include <iostream>
template<size_t N>
constexpr auto generateFibonacci() {
    std::array<int, N> result{};
    if (N > 0) result[0] = 0;
    if (N > 1) result[1] = 1;
    
    for (size_t i = 2; i < N; ++i) {
        result[i] = result[i - 1] + result[i - 2];
    }
    
    return result;
}
int main() {
    constexpr auto fib = generateFibonacci<10>();
    
    for (int x : fib) {
        std::cout << x << " ";  // 0 1 1 2 3 5 8 13 21 34
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

0 1 1 2 3 5 8 13 21 34

Key: Compile-time array generation creates lookup tables with zero runtime cost.

3. Template Metaprogramming (TMP)

#include <iostream>
// Recursive template
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
    static constexpr int value = 1;
};
int main() {
    std::cout << Factorial<5>::value << std::endl;  // 120 (compile-time)
    
    return 0;
}

Output:

120

Key: TMP uses template recursion for compile-time computation. Prefer constexpr functions for readability.

Performance Comparison

Benchmark: Fibonacci

#include <chrono>
#include <iostream>
// Runtime
int runtimeFib(int n) {
    if (n <= 1) return n;
    return runtimeFib(n - 1) + runtimeFib(n - 2);
}
// Compile-time
constexpr int compiletimeFib(int n) {
    if (n <= 1) return n;
    return compiletimeFib(n - 1) + compiletimeFib(n - 2);
}
int main() {
    // Runtime (slow)
    auto start = std::chrono::high_resolution_clock::now();
    int r = runtimeFib(40);
    auto end = std::chrono::high_resolution_clock::now();
    auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    
    std::cout << "Runtime: " << time << "ms" << std::endl;
    
    // Compile-time (instant)
    constexpr int c = compiletimeFib(40);
    std::cout << "Compile-time: 0ms (pre-computed)" << std::endl;
    
    return 0;
}

Results:

MethodTime
Runtime1200ms
Compile-time0ms
Conclusion: Compile-time computation is dramatically faster at runtime (but increases build time).

Production Patterns

Pattern 1: Lookup Table Generation

#include <array>
#include <iostream>
#include <cmath>
constexpr auto generateSinTable() {
    constexpr size_t SIZE = 360;
    std::array<double, SIZE> table{};
    
    constexpr double PI = 3.14159265358979323846;
    
    for (size_t i = 0; i < SIZE; ++i) {
        double radians = i * PI / 180.0;
        // In real code: table[i] = std::sin(radians);
        // (std::sin is not constexpr in C++17, but is in C++26)
        table[i] = radians;
    }
    
    return table;
}
int main() {
    constexpr auto sinTable = generateSinTable();
    
    std::cout << sinTable[45] << std::endl;  // 0.785398 (45 degrees)
    
    return 0;
}

Key: Pre-compute trigonometric tables at compile time for real-time graphics/physics.

Pattern 2: Type Checking

#include <iostream>
#include <type_traits>
template<typename T>
constexpr bool isNumeric() {
    if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
        return true;
    } else {
        return false;
    }
}
template<typename T>
void process(T value) {
    if constexpr (isNumeric<T>()) {
        std::cout << "Number: " << value << std::endl;
    } else {
        std::cout << "String: " << value << std::endl;
    }
}
int main() {
    process(42);       // Number: 42
    process("hello");  // String: hello
    
    return 0;
}

Output:

Number: 42
String: hello

Pattern 3: Compile-Time Sorting

#include <algorithm>
#include <array>
#include <iostream>
constexpr void bubbleSort(int* arr, int n) {
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
constexpr auto getSortedArray() {
    std::array<int, 5> arr = {5, 2, 8, 1, 9};
    bubbleSort(arr.data(), arr.size());
    return arr;
}
int main() {
    constexpr auto sorted = getSortedArray();
    
    for (int x : sorted) {
        std::cout << x << " ";  // 1 2 5 8 9
    }
    std::cout << std::endl;
    
    return 0;
}

Output:

1 2 5 8 9

Pattern 4: Bit Manipulation

#include <iostream>
constexpr unsigned int reverseBits(unsigned int n) {
    unsigned int result = 0;
    for (int i = 0; i < 32; ++i) {
        result <<= 1;
        result |= (n & 1);
        n >>= 1;
    }
    return result;
}
int main() {
    constexpr unsigned int reversed = reverseBits(0b10110000);
    
    std::cout << std::hex << reversed << std::endl;
    
    return 0;
}

Key: Compile-time bit manipulation for embedded systems and cryptography.

Common Issues

Issue 1: constexpr Constraints

// ❌ Static variables not allowed (pre-C++23)
constexpr int bad() {
    static int x = 0;  // Error
    return x++;
}
// ❌ Dynamic allocation not allowed (C++17)
constexpr int bad2() {
    int* p = new int(10);  // Error
    return *p;
}
// ✅ OK
constexpr int good(int x) {
    return x * 2;
}

Key: constexpr functions have restrictions—no static locals (pre-C++23), limited dynamic allocation.

Issue 2: Runtime Values

#include <iostream>
int main() {
    int x;
    std::cin >> x;
    
    // ❌ Error: runtime value
    constexpr int y = x * 2;
    
    // ✅ OK: runtime variable
    int y2 = x * 2;
    
    return 0;
}

Key: constexpr requires compile-time known values.

Issue 3: Complex Computation

// ❌ Slow compile time
constexpr int slowFib(int n) {
    if (n <= 1) return n;
    return slowFib(n - 1) + slowFib(n - 2);
}
constexpr int x = slowFib(50);  // Very slow compilation!
// ✅ Memoization
constexpr auto fastFib() {
    std::array<int, 50> result{};
    result[0] = 0;
    result[1] = 1;
    for (int i = 2; i < 50; ++i) {
        result[i] = result[i - 1] + result[i - 2];
    }
    return result;
}
constexpr auto fibTable = fastFib();
constexpr int y = fibTable[49];  // Fast

Key: Use iterative algorithms or memoization to avoid exponential compile-time complexity.

Issue 4: constexpr vs consteval Confusion

// constexpr: compile-time or runtime
constexpr int square(int n) {
    return n * n;
}
int x = 10;
int y = square(x);  // ✅ Runs at runtime
// consteval: compile-time only
consteval int sqr(int n) {
    return n * n;
}
constexpr int z = sqr(5);  // ✅ OK
// int w = sqr(x);  // ❌ Error: runtime value

Key: Use consteval to enforce compile-time-only intent.

Template Metaprogramming (TMP)

Recursive Templates

#include <iostream>
// Recursive template
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
    static constexpr int value = 1;
};
int main() {
    std::cout << Factorial<5>::value << std::endl;  // 120 (compile-time)
    
    return 0;
}

Output:

120

Key: TMP uses template specialization for compile-time recursion. Prefer constexpr functions for readability.

TMP vs constexpr

// TMP (verbose)
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};
// constexpr (readable)
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

Recommendation: Use constexpr functions for most compile-time computation; reserve TMP for type-level programming.

Production Use Cases

Use Case 1: Configuration Tables

#include <array>
constexpr auto generateConfigTable() {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        table[i] = i * 2;  // Example: multiply by 2
    }
    return table;
}
constexpr auto configTable = generateConfigTable();
int main() {
    int value = configTable[42];  // Instant lookup
}

Key: Pre-compute configuration tables for embedded systems.

Use Case 2: Type Dispatch

#include <iostream>
#include <type_traits>
template<typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "Integer: " << value << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Float: " << value << std::endl;
    } else {
        std::cout << "Other: " << value << std::endl;
    }
}
int main() {
    process(42);       // Integer: 42
    process(3.14);     // Float: 3.14
    process("hello");  // Other: hello
    
    return 0;
}

Output:

Integer: 42
Float: 3.14
Other: hello

Use Case 3: Compile-Time Validation

#include <array>
template<size_t N>
constexpr bool isPowerOfTwo() {
    return N > 0 && (N & (N - 1)) == 0;
}
template<size_t N>
class Buffer {
    static_assert(isPowerOfTwo<N>(), "Buffer size must be power of 2");
    std::array<char, N> data;
};
int main() {
    Buffer<256> buf1;  // ✅ OK
    // Buffer<100> buf2;  // ❌ Error: not power of 2
}

Key: Use static_assert with constexpr functions for compile-time validation.

Performance Benchmarks

Compile-Time vs Runtime

// Benchmark: Fibonacci(40)
// Runtime: 1200ms
// Compile-time: 0ms (pre-computed)

Key: Compile-time computation eliminates runtime overhead but increases build time.

Compile Time Impact

// Simple constexpr: +0.1s compile time
constexpr int x = factorial(10);
// Complex constexpr: +5s compile time
constexpr auto table = generateLargeTable<10000>();

Trade-off: Balance compile-time work with build time constraints.

Best Practices

1. Use constexpr for Pure Functions

// ✅ Pure function: good constexpr candidate
constexpr int add(int a, int b) {
    return a + b;
}
// ❌ Side effects: not constexpr
int addAndLog(int a, int b) {
    std::cout << "Adding" << std::endl;  // I/O not allowed
    return a + b;
}

2. Prefer constexpr Over TMP

// ❌ TMP (verbose)
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};
// ✅ constexpr (readable)
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

3. Use consteval for Compile-Time-Only Intent

// ✅ Enforce compile-time
consteval int configValue(int id) {
    return id * 100;
}
constexpr int x = configValue(5);  // OK
// int y = configValue(runtimeValue);  // Error

4. Avoid Deep Recursion

// ❌ Deep recursion (slow compile)
constexpr int fib(int n) {
    return n <= 1 ? n : fib(n-1) + fib(n-2);
}
// ✅ Iterative (fast compile)
constexpr int fib(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        int temp = a + b;
        a = b;
        b = temp;
    }
    return a;
}

5. Use static_assert for Documentation

template<typename T>
void process(T value) {
    static_assert(std::is_arithmetic_v<T>, "T must be numeric");
    // ...
}

Summary

Key Points

  1. constexpr
    • Compile-time or runtime execution
    • Variables, functions, classes
    • Array sizes, template arguments
  2. consteval (C++20)
    • Compile-time-only execution
    • Immediate functions
    • No runtime values allowed
  3. if constexpr (C++17)
    • Compile-time branching
    • Type-based conditional compilation
    • Replaces template specialization
  4. Performance
    • Compile-time: zero runtime overhead
    • Compile time may increase
    • Use memoization for complex computation

Selection Guide

ScenarioRecommendationReason
Compile-time constantconstexprArray sizes, etc.
Compile-time-onlyconstevalClear intent
Type-based branchingif constexprConditional compilation
Runtime also neededconstexprFlexibility

Cheat Sheet

// constexpr variable
constexpr int MAX_SIZE = 100;
// constexpr function
constexpr int square(int x) {
    return x * x;
}
// constexpr class
class Point {
public:
    constexpr Point(int x, int y) : x_(x), y_(y) {}
    constexpr int getX() const { return x_; }
private:
    int x_, y_;
};
// if constexpr
template<typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;
    } else {
        return t;
    }
}
// consteval
consteval int sqr(int n) {
    return n * n;
}

Compile-Time Programming Checklist

  • Pure functions marked constexpr?
  • Using consteval for compile-time-only intent?
  • Avoiding deep recursion?
  • Using static_assert for validation?
  • Balancing compile time vs runtime performance?

Keywords

C++ constexpr, consteval, compile-time programming, template metaprogramming, TMP, if constexpr, immediate functions One-line summary: Compile-time programming with constexpr, consteval, and if constexpr eliminates runtime overhead by pre-computing values and enabling type-based conditional compilation.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Master C++ compile-time programming: constexpr variables/functions/classes, consteval immediate functions, if.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, constexpr, compile-time, TMP, metaprogramming, consteval, if constexpr.