C++ constexpr Lambda | 'Compile-Time Lambda' Guide

C++ constexpr Lambda | 'Compile-Time Lambda' Guide

이 글의 핵심

C++ constexpr Lambda: "Compile-Time Lambda" Guide. Constexpr lambda basics and compile-time calculation.

Introduction

C++17 constexpr lambda is a lambda expression executable at compile-time. Used for metaprogramming, compile-time calculation, type validation, etc., providing powerful features without runtime cost.


Reality in Production

When learning development, everything is clean and theoretical. But production is different. You wrestle with legacy code, chase tight deadlines, and face unexpected bugs. The content covered in this guide was initially learned as theory, but I realized “ah, that’s why it’s designed this way” while applying it to actual projects.

What stands out in my memory is the trial and error from my first project. I did it as I learned from books but spent days not knowing why it didn’t work. Eventually, I found the problem through a senior developer’s code review and learned a lot in the process. This guide covers not only theory but also pitfalls you may encounter in practice and their solutions.

1. constexpr Lambda Basics

C++17 Implicit constexpr

Here is detailed implementation code using C++. Import the necessary modules. Understand the role of each part while examining the code.

#include <iostream>

// C++17: lambda implicitly constexpr
auto add = [](int a, int b) {
    return a + b;
};

int main() {
    // Compile-time use
    constexpr int result1 = add(3, 4);  // 7
    static_assert(add(3, 4) == 7);
    
    // Runtime use also possible
    int x = 10, y = 20;
    int result2 = add(x, y);  // 30
    
    std::cout << result1 << ", " << result2 << std::endl;
    return 0;
}

Key Concepts:

  • From C++17, lambdas are implicitly constexpr
  • Condition: lambda body satisfies constexpr requirements
  • Usable at both compile-time and runtime

Explicit constexpr

Below is an implementation example using C++. Understand the role of each part while examining the code.

// Explicitly specify constexpr
constexpr auto square = [](int x) constexpr {
    return x * x;
};

constexpr int result = square(5);  // 25
static_assert(square(5) == 25);

// Use as array size
int arr[square(4)];  // Size 16

2. Compile-Time Calculation

Example 1: Factorial

Here is detailed implementation code using C++. Import the necessary modules and process data with loops. Understand the role of each part while examining the code.

#include <iostream>

constexpr auto factorial = [](int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
};

int main() {
    // Compile-time calculation
    constexpr int f5 = factorial(5);  // 120
    static_assert(factorial(5) == 120);
    
    // Use as array size
    int arr[factorial(4)];  // Size 24
    
    std::cout << "5! = " << f5 << std::endl;
    std::cout << "Array size: " << sizeof(arr) / sizeof(int) << std::endl;
    
    return 0;
}

Example 2: Power (Using Template)

Here is detailed implementation code using C++. Import the necessary modules and process data with loops. Understand the role of each part while examining the code.

#include <iostream>

template<int N>
constexpr auto power = [](int base) {
    int result = 1;
    for (int i = 0; i < N; i++) {
        result *= base;
    }
    return result;
};

int main() {
    constexpr int p2 = power<3>(2);  // 2^3 = 8
    constexpr int p3 = power<5>(3);  // 3^5 = 243
    
    static_assert(power<3>(2) == 8);
    static_assert(power<5>(3) == 243);
    
    std::cout << "2^3 = " << p2 << std::endl;
    std::cout << "3^5 = " << p3 << std::endl;
    
    return 0;
}

Example 3: Array Initialization

Here is detailed implementation code using C++. Import the necessary modules and process data with loops. Understand the role of each part while examining the code.

#include <array>
#include <iostream>

template<size_t N>
constexpr auto makeArray = []() {
    std::array<int, N> arr{};
    for (size_t i = 0; i < N; i++) {
        arr[i] = i * i;
    }
    return arr;
};

int main() {
    constexpr auto squares = makeArray<5>();
    // {0, 1, 4, 9, 16}
    
    for (int val : squares) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

3. Type Checking and Metaprogramming

Type Checking

Here is detailed implementation code using C++. Import the necessary modules. Understand the role of each part while examining the code.

#include <type_traits>
#include <iostream>

constexpr auto isIntegral = [](auto value) {
    return std::is_integral_v<decltype(value)>;
};

constexpr auto isFloating = [](auto value) {
    return std::is_floating_point_v<decltype(value)>;
};

int main() {
    static_assert(isIntegral(10));
    static_assert(!isIntegral(3.14));
    static_assert(isFloating(3.14));
    static_assert(!isFloating(10));
    
    std::cout << "Type check passed" << std::endl;
    return 0;
}

Conditional Compilation

Here is detailed implementation code using C++. Import the necessary modules and perform branching with conditionals. Understand the role of each part while examining the code.

#include <type_traits>
#include <iostream>

constexpr auto processValue = [](auto value) {
    if constexpr (std::is_integral_v<decltype(value)>) {
        return value * 2;
    } else if constexpr (std::is_floating_point_v<decltype(value)>) {
        return value * 1.5;
    } else {
        return value;
    }
};

int main() {
    constexpr int i = processValue(10);      // 20
    constexpr double d = processValue(10.0); // 15.0
    
    static_assert(i == 20);
    static_assert(d == 15.0);
    
    std::cout << i << ", " << d << std::endl;
    return 0;
}

4. Constraints

What’s Allowed

Here is detailed implementation code using C++. Process data with loops. Understand the role of each part while examining the code.

// ✅ Allowed: basic operations
constexpr auto add = [](int a, int b) { return a + b; };

// ✅ Allowed: loops
constexpr auto sum = [](int n) {
    int result = 0;
    for (int i = 1; i <= n; i++) {
        result += i;
    }
    return result;
};

// ✅ Allowed: recursion
constexpr auto fibonacci = [](int n) {
    auto fib = [](int n, auto& self) -> int {
        if (n <= 1) return n;
        return self(n - 1, self) + self(n - 2, self);
    };
    return fib(n, fib);
};

// ✅ Allowed: capture (C++17)
constexpr int x = 10;
constexpr auto addX = [x](int y) { return x + y; };

What’s Not Allowed

Here is the nonConstexpr implementation:

// ❌ Not allowed: non-constexpr function call
int nonConstexpr(int x) { return x * 2; }
constexpr auto bad1 = [](int x) {
    return nonConstexpr(x);  // Error
};

// ❌ Not allowed: static variable (C++17)
constexpr auto bad2 = []() {
    static int count = 0;  // Error
    return count++;
};

// ❌ Not allowed: dynamic allocation
constexpr auto bad3 = []() {
    int* p = new int(10);  // Error
    delete p;
    return 0;
};

// ❌ Not allowed: I/O
constexpr auto bad4 = []() {
    std::cout << "Hello";  // Error
    return 0;
};

Summary

Key Points

  1. C++17 constexpr lambda: Implicitly constexpr
  2. Compile-time calculation: No runtime cost
  3. Type checking: Powerful with type_traits
  4. Metaprogramming: Template + lambda combination
  5. Constraints: No I/O, dynamic allocation

When to Use

Use constexpr lambda when:

  • Need compile-time calculation
  • Metaprogramming
  • Type validation
  • Performance optimization

Don’t use when:

  • Need I/O operations
  • Dynamic allocation required
  • Too complex (readability matters)

Best Practices

  • ✅ Use for compile-time calculation
  • ✅ Combine with templates
  • ✅ Use for type checking
  • ❌ Don’t overuse (readability first)
  • ❌ Don’t use for side effects

Master compile-time programming with constexpr lambda! 🚀