Complete Guide to C++20 consteval | Compile-Time Only Functions

Complete Guide to C++20 consteval | Compile-Time Only Functions

이 글의 핵심

consteval immediate functions: must run at compile time—stricter than constexpr for APIs that must never execute at runtime.

What is consteval? Why do we need it?

Problem Scenario: The ambiguity of constexpr

Problem: A constexpr function can execute at compile-time or runtime, which makes it ambiguous when you want to enforce compile-time calculations.

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int x = factorial(5);  // Compile-time: 120
    
    int n = 10;
    int y = factorial(n);  // Runtime (possibly unintended)
}

Solution: consteval ensures that a function is compile-time only. Passing runtime values to it results in a compile error, guaranteeing compile-time evaluation.

consteval int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int x = factorial(5);  // OK: 120
    
    int n = 10;
    // int y = factorial(n);  // Error: n is a runtime value
}
flowchart TD
    subgraph constexpr["constexpr"]
        ce1["Compile-time OK"]
        ce2["Runtime OK"]
    end
    subgraph consteval["consteval"]
        cv1["Compile-time OK"]
        cv2["Runtime Error"]
    end

Table of Contents

  1. constexpr vs consteval
  2. Immediate Functions
  3. Practical Applications: Compile-Time Validation
  4. Metaprogramming
  5. Common Errors and Solutions
  6. Production Patterns
  7. Complete Example: Compile-Time Configuration System

1. constexpr vs consteval

Comparison

Featureconstexprconsteval
Compile-time executionPossibleMandatory
Runtime executionPossibleNot allowed
Use caseFlexible calculationsGuaranteed compile-time
ErrorsRuntime values OKRuntime values cause error

Examples

// constexpr: Both compile-time and runtime allowed
constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    constexpr int x = add(1, 2);  // Compile-time
    
    int a = 5;
    int y = add(a, 10);  // Runtime (OK)
}

// consteval: Compile-time only
consteval int multiply(int a, int b) {
    return a * b;
}

int main() {
    constexpr int x = multiply(2, 3);  // OK
    
    int a = 5;
    // int y = multiply(a, 10);  // Error: a is a runtime value
}

2. Immediate Functions

What is an Immediate Function?

A consteval function is referred to as an immediate function, meaning it must be evaluated immediately at the point of invocation.

consteval int square(int x) {
    return x * x;
}

// consteval functions can only be called within consteval functions
consteval int sum_of_squares(int a, int b) {
    return square(a) + square(b);  // OK
}

// Cannot call consteval from constexpr
constexpr int wrapper(int x) {
    // return square(x);  // Error: consteval in constexpr
    return x * 2;
}

int main() {
    constexpr int result = sum_of_squares(3, 4);  // 25
}

3. Practical Applications: Compile-Time Validation

Range Validation

consteval int check_range(int value, int min, int max) {
    if (value < min || value > max) {
        throw "Value out of range";
    }
    return value;
}

int main() {
    constexpr int size = check_range(100, 1, 1024);  // OK
    int buffer[size];
    
    // constexpr int bad = check_range(2000, 1, 1024);  // Compile error
}

String Hashing

#include <string_view>

consteval unsigned int hash(std::string_view str) {
    unsigned int hash = 5381;
    for (char c : str) {
        hash = ((hash << 5) + hash) + static_cast<unsigned char>(c);
    }
    return hash;
}

enum class MessageType : unsigned int {
    Login = hash("login"),
    Logout = hash("logout"),
    Data = hash("data")
};

void handle_message(const std::string& type) {
    switch (hash(type.c_str())) {  // Compile-time hash
    case hash("login"):
        std::cout << "Login\n";
        break;
    case hash("logout"):
        std::cout << "Logout\n";
        break;
    case hash("data"):
        std::cout << "Data\n";
        break;
    }
}

Type Size Validation

template<typename T>
consteval bool is_small_type() {
    return sizeof(T) <= 16;
}

template<typename T>
    requires is_small_type<T>()
void process(T value) {
    // T is guaranteed to be 16 bytes or less
}

int main() {
    process(42);        // OK: sizeof(int) = 4
    process(3.14);      // OK: sizeof(double) = 8
    // process(std::string{"hello"});  // Error: sizeof(string) > 16
}

4. Metaprogramming

Compile-Time Factorial

consteval int factorial(int n) {
    if (n < 0) throw "Negative factorial";
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int f5 = factorial(5);   // 120
    constexpr int f10 = factorial(10); // 3628800
    
    // Use as array size
    int buffer[factorial(4)];  // 24 elements
}

Compile-Time String Processing

#include <array>
#include <string_view>

consteval std::size_t count_chars(std::string_view str, char c) {
    std::size_t count = 0;
    for (char ch : str) {
        if (ch == c) ++count;
    }
    return count;
}

int main() {
    constexpr auto count = count_chars("hello world", 'l');
    static_assert(count == 3);
    
    std::array<char, count> buffer;  // 3 elements
}

Compile-Time Configuration

consteval int get_buffer_size() {
    #ifdef LARGE_BUFFER
        return 1024 * 1024;  // 1MB
    #else
        return 4096;         // 4KB
    #endif
}

consteval int get_thread_count() {
    return 8;  // Compile-time constant
}

int main() {
    constexpr int buffer_size = get_buffer_size();
    constexpr int threads = get_thread_count();
    
    char buffer[buffer_size];
    std::array<std::thread, threads> thread_pool;
}

5. Common Errors and Solutions

Issue 1: Passing Runtime Values

Symptom: error: call to consteval function is not a constant expression.

consteval int square(int x) {
    return x * x;
}

int main() {
    int a = 5;
    // int b = square(a);  // Error: a is a runtime value
    
    // ✅ Solution 1: Use constexpr variables
    constexpr int c = 5;
    int d = square(c);  // OK
    
    // ✅ Solution 2: Use literals
    int e = square(5);  // OK
}

Issue 2: Side Effects

Symptom: error: call to non-constexpr function.

Cause: consteval functions cannot perform side effects like I/O or dynamic memory allocation.

// ❌ Incorrect usage
consteval int bad() {
    std::cout << "Hello\n";  // Error: I/O
    return 42;
}

// ✅ Correct usage: Pure calculations only
consteval int good(int x) {
    return x * 2;
}

Issue 3: Calling consteval from constexpr

Cause: constexpr functions can execute at runtime, so calling consteval functions from them is not allowed.

consteval int immediate() {
    return 42;
}

// ❌ Incorrect usage
constexpr int wrapper() {
    return immediate();  // Error
}

// ✅ Correct usage: Call within consteval
consteval int wrapper2() {
    return immediate();  // OK
}

// ✅ Or use constexpr if for branching
constexpr int wrapper3(int x) {
    if (std::is_constant_evaluated()) {
        return 42;  // Compile-time
    } else {
        return x * 2;  // Runtime
    }
}

6. Production Patterns

Pattern 1: Compile-Time String Validation

consteval bool is_valid_identifier(std::string_view str) {
    if (str.empty()) return false;
    if (!std::isalpha(str[0]) && str[0] != '_') return false;
    
    for (char c : str) {
        if (!std::isalnum(c) && c != '_') return false;
    }
    return true;
}

template<std::size_t N>
consteval auto make_identifier(const char (&str)[N]) {
    if (!is_valid_identifier(str)) {
        throw "Invalid identifier";
    }
    return std::string_view(str, N - 1);
}

int main() {
    constexpr auto id1 = make_identifier("valid_name");  // OK
    // constexpr auto id2 = make_identifier("123invalid");  // Compile error
}

Pattern 2: Compile-Time Lookup Table

#include <array>

consteval std::array<int, 256> generate_crc_table() {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        int crc = i;
        for (int j = 0; j < 8; ++j) {
            crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
        }
        table[i] = crc;
    }
    return table;
}

constexpr auto CRC_TABLE = generate_crc_table();

unsigned int crc32(const char* data, std::size_t length) {
    unsigned int crc = 0xFFFFFFFF;
    for (std::size_t i = 0; i < length; ++i) {
        crc = (crc >> 8) ^ CRC_TABLE[(crc ^ data[i]) & 0xFF];
    }
    return ~crc;
}

Pattern 3: Configuration Calculations

consteval int calculate_pool_size() {
    #ifdef PRODUCTION
        return 128;
    #elif defined(STAGING)
        return 64;
    #else
        return 16;
    #endif
}

consteval int calculate_timeout_ms() {
    return 30 * 1000;  // 30 seconds
}

int main() {
    constexpr int pool_size = calculate_pool_size();
    constexpr int timeout = calculate_timeout_ms();
    
    static_assert(pool_size > 0);
    static_assert(timeout > 0);
}

7. Complete Example: Compile-Time Configuration System

#include <string_view>
#include <array>

// Configuration key-value pairs
struct ConfigEntry {
    std::string_view key;
    int value;
};

// Generate compile-time configuration
consteval auto generate_config() {
    std::array<ConfigEntry, 3> config{{
        {"max_connections", 1000},
        {"timeout_ms", 30000},
        {"buffer_size", 4096}
    }};
    return config;
}

// Retrieve compile-time configuration
consteval int get_config(std::string_view key) {
    constexpr auto config = generate_config();
    for (const auto& entry : config) {
        if (entry.key == key) {
            return entry.value;
        }
    }
    throw "Config key not found";
}

int main() {
    constexpr int max_conn = get_config("max_connections");  // 1000
    constexpr int timeout = get_config("timeout_ms");        // 30000
    
    std::array<int, max_conn> connection_pool;
    
    static_assert(max_conn == 1000);
    static_assert(timeout == 30000);
}

Summary

ConceptDescription
constevalCompile-time only functions
Immediate FunctionsEvaluated immediately at call site
Difference from constexprconsteval cannot execute at runtime
Use casesCompile-time validation, metaprogramming

consteval enforces compile-time calculations, enabling complex computations without runtime overhead.


FAQ

Q1: When should I use consteval vs constexpr?

A: Use consteval when you want to enforce compile-time calculations. Use constexpr for flexibility between compile-time and runtime.

Q2: Can a consteval function be called from a constexpr function?

A: No. consteval functions can only be called within other consteval functions. constexpr functions may execute at runtime, so calling consteval from them results in an error.

Q3: What happens if I pass runtime values?

A: A compile error occurs. All arguments to consteval functions must be compile-time constants.

Q4: What if the function has side effects?

A: Functions with side effects like I/O, dynamic allocation, or global variable modification cannot be used in consteval. Only pure calculations are allowed.

Q5: What is the compiler support for consteval?

A:

  • GCC 10+: Fully supported
  • Clang 10+: Fully supported
  • MSVC 2019 (16.10+): Fully supported

Q6: Where can I learn more about consteval?

A:

One-line summary: consteval enforces compile-time calculations. Next, check out constexpr for more details.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ static_assert | “정적 단언” 가이드
  • C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대


이 글에서 다루는 키워드 (관련 검색어)

C++, consteval, cpp20, compile-time, constexpr, metaprogramming 등으로 검색하시면 이 글이 도움이 됩니다.