C++ constexpr Lambda | "컴파일 타임 람다" 가이드
이 글의 핵심
C++ constexpr Lambda에 대한 실전 가이드입니다.
들어가며
C++17 constexpr 람다는 컴파일 타임에 실행 가능한 람다 표현식입니다. 메타프로그래밍, 컴파일 타임 계산, 타입 검증 등에 활용되며, 런타임 비용 없이 강력한 기능을 제공합니다.
1. constexpr 람다 기본
C++17 암시적 constexpr
#include <iostream>
// C++17: 람다가 암시적으로 constexpr
auto add = {
return a + b;
};
int main() {
// 컴파일 타임 사용
constexpr int result1 = add(3, 4); // 7
static_assert(add(3, 4) == 7);
// 런타임 사용도 가능
int x = 10, y = 20;
int result2 = add(x, y); // 30
std::cout << result1 << ", " << result2 << std::endl;
return 0;
}
핵심 개념:
- C++17부터 람다가 암시적으로 constexpr
- 조건: 람다 본문이
constexpr요구사항을 만족하면 - 컴파일 타임과 런타임 모두 사용 가능
명시적 constexpr
// 명시적으로 constexpr 지정
constexpr auto square = constexpr {
return x * x;
};
constexpr int result = square(5); // 25
static_assert(square(5) == 25);
// 배열 크기로 사용
int arr[square(4)]; // 크기 16
2. 컴파일 타임 계산
예제 1: Factorial
#include <iostream>
constexpr auto factorial = {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
};
int main() {
// 컴파일 타임 계산
constexpr int f5 = factorial(5); // 120
static_assert(factorial(5) == 120);
// 배열 크기로 사용
int arr[factorial(4)]; // 크기 24
std::cout << "5! = " << f5 << std::endl;
std::cout << "배열 크기: " << sizeof(arr) / sizeof(int) << std::endl;
return 0;
}
예제 2: 거듭제곱 (템플릿 활용)
#include <iostream>
template<int N>
constexpr auto power = {
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;
}
예제 3: 배열 초기화
#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. 타입 검사 및 메타프로그래밍
타입 검사
#include <type_traits>
#include <iostream>
constexpr auto isIntegral = {
return std::is_integral_v<decltype(value)>;
};
constexpr auto isFloating = {
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 << "타입 검사 통과" << std::endl;
return 0;
}
조건부 컴파일
#include <type_traits>
#include <iostream>
constexpr auto processValue = {
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. 제약 사항
허용되는 것
// ✅ 허용: 기본 연산
constexpr auto add = {
return x + y;
};
// ✅ 허용: 제어문
constexpr auto max = {
return (a > b) ? a : b;
};
// ✅ 허용: 루프
constexpr auto sum = {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
return result;
};
// ✅ 허용: constexpr 변수 캡처
constexpr int multiplier = 10;
constexpr auto scale = [multiplier](int x) {
return x * multiplier;
};
허용되지 않는 것
// ❌ 불가능: static 지역 변수
constexpr auto invalid1 = {
static int count = 0; // 에러!
return x;
};
// ❌ 불가능: 예외 던지기
constexpr auto invalid2 = {
if (x < 0) {
throw std::runtime_error("음수"); // 에러!
}
return x;
};
// ❌ 불가능: 비constexpr 함수 호출
void nonConstexprFunc() { }
constexpr auto invalid3 = {
nonConstexprFunc(); // 에러!
return 0;
};
// ❌ 불가능: 비constexpr 변수 캡처 (컴파일 타임 사용 시)
int nonConstexpr = 10;
constexpr auto invalid4 = [nonConstexpr]() {
return nonConstexpr; // 런타임에서는 OK, 컴파일 타임에서는 에러
};
// constexpr int x = invalid4(); // 에러!
5. 자주 발생하는 문제
문제 1: 비constexpr 변수 캡처
#include <iostream>
int main() {
int x = 10; // 비constexpr 변수
// ❌ 컴파일 타임 사용 불가
constexpr auto lambda1 = [x]() {
return x * 2;
};
// constexpr int result = lambda1(); // 에러!
// ✅ 런타임 사용은 가능
int runtimeResult = lambda1(); // OK
// ✅ constexpr 변수 캡처
constexpr int y = 10;
constexpr auto lambda2 = [y]() {
return y * 2;
};
constexpr int compileTimeResult = lambda2(); // OK
std::cout << runtimeResult << ", " << compileTimeResult << std::endl;
return 0;
}
해결책: 컴파일 타임에 사용하려면 캡처하는 변수도 constexpr이어야 합니다.
문제 2: 재귀 람다
#include <functional>
#include <iostream>
// ❌ 람다 재귀 (C++17)
// auto factorial = {
// return n <= 1 ? 1 : n * factorial(n - 1); // 에러: factorial 미정의
// };
// ✅ std::function 사용
std::function<int(int)> factorial = [&](int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
};
// ✅ C++23: 명시적 this (Deducing this)
// auto factorial = {
// return n <= 1 ? 1 : n * self(n - 1);
// };
int main() {
std::cout << "5! = " << factorial(5) << std::endl; // 120
return 0;
}
해결책: C++17에서는 std::function을 사용하거나, C++23의 명시적 this를 사용하세요.
문제 3: static_assert 실패
constexpr auto divide = {
return a / b;
};
// ❌ 0으로 나누기
// static_assert(divide(10, 0) == 0); // 컴파일 에러!
// ✅ 조건 검사
constexpr auto safeDivide = {
return (b != 0) ? a / b : 0;
};
static_assert(safeDivide(10, 0) == 0); // OK
static_assert(safeDivide(10, 2) == 5); // OK
문제 4: 타입 추론 혼동
#include <iostream>
constexpr auto add = {
return a + b;
};
int main() {
// 타입이 다르면 결과 타입도 달라짐
constexpr int r1 = add(1, 2); // int + int = int
constexpr double r2 = add(1.5, 2.5); // double + double = double
constexpr double r3 = add(1, 2.5); // int + double = double
static_assert(r1 == 3);
static_assert(r2 == 4.0);
static_assert(r3 == 3.5);
std::cout << r1 << ", " << r2 << ", " << r3 << std::endl;
return 0;
}
6. 활용 패턴
패턴 1: 컴파일 타임 검증
#include <type_traits>
// 범위 검증
constexpr auto inRange = {
return value >= min && value <= max;
};
static_assert(inRange(50, 0, 100));
// static_assert(inRange(150, 0, 100)); // 컴파일 에러!
// 타입 검증
constexpr auto isNumeric = {
using T = decltype(value);
return std::is_arithmetic_v<T>;
};
static_assert(isNumeric(10));
static_assert(isNumeric(3.14));
// static_assert(isNumeric("text")); // 컴파일 에러!
패턴 2: 컴파일 타임 룩업 테이블
#include <array>
#include <iostream>
template<size_t N>
constexpr auto makeLookupTable = {
std::array<int, N> table{};
for (size_t i = 0; i < N; i++) {
table[i] = i * i * i; // 세제곱
}
return table;
};
constexpr auto cubes = makeLookupTable<10>();
int main() {
std::cout << "5^3 = " << cubes[5] << std::endl; // 125
return 0;
}
패턴 3: 타입 변환 유틸리티
#include <type_traits>
#include <iostream>
constexpr auto toInt = {
return static_cast<int>(value);
};
constexpr auto toDouble = {
return static_cast<double>(value);
};
int main() {
constexpr int i = toInt(3.14); // 3
constexpr double d = toDouble(10); // 10.0
static_assert(i == 3);
static_assert(d == 10.0);
std::cout << i << ", " << d << std::endl;
return 0;
}
7. 실전 예제: 컴파일 타임 수학 라이브러리
#include <iostream>
#include <array>
#include <cmath>
namespace CompileTimeMath {
// 거듭제곱
constexpr auto pow = {
int result = 1;
for (int i = 0; i < exp; i++) {
result *= base;
}
return result;
};
// Factorial
constexpr auto factorial = {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
};
// 피보나치
constexpr auto fibonacci = {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
};
// 소수 판별
constexpr auto isPrime = {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
};
// 최대공약수 (GCD)
constexpr auto gcd = {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
};
}
int main() {
using namespace CompileTimeMath;
// 모두 컴파일 타임에 계산됨
constexpr int p = pow(2, 10); // 1024
constexpr int f = factorial(6); // 720
constexpr int fib = fibonacci(10); // 55
constexpr bool prime = isPrime(17); // true
constexpr int g = gcd(48, 18); // 6
static_assert(p == 1024);
static_assert(f == 720);
static_assert(fib == 55);
static_assert(prime);
static_assert(g == 6);
std::cout << "2^10 = " << p << std::endl;
std::cout << "6! = " << f << std::endl;
std::cout << "fib(10) = " << fib << std::endl;
std::cout << "17은 소수? " << (prime ? "예" : "아니오") << std::endl;
std::cout << "gcd(48, 18) = " << g << std::endl;
return 0;
}
정리
핵심 요약
- C++17: 람다가 암시적으로
constexpr - 컴파일 타임: 상수 표현식에서 사용 가능
- 런타임: 일반 람다처럼 사용 가능
- 캡처:
constexpr변수만 컴파일 타임 사용 - 제약: static 변수, 예외, 비constexpr 함수 호출 불가
- 재귀:
std::function또는 C++23 명시적this
constexpr 람다 vs 일반 람다
| 특징 | constexpr 람다 | 일반 람다 |
|---|---|---|
| 컴파일 타임 실행 | ✓ | ✗ |
| 런타임 실행 | ✓ | ✓ |
| static 변수 | ✗ | ✓ |
| 예외 | ✗ | ✓ |
| 성능 | 런타임 비용 없음 | 런타임 실행 |
| 타입 안전성 | 컴파일 타임 검증 | 런타임 검증 |
실전 팁
사용 시기:
- 컴파일 타임 상수 계산 (배열 크기, 템플릿 인자)
- 타입 검증 및 메타프로그래밍
- 룩업 테이블 생성
static_assert로 조건 검증
성능:
- 컴파일 타임 계산으로 런타임 비용 제거
- 복잡한 계산은 컴파일 시간 증가 가능
- 적절한 균형 유지
주의사항:
- 비constexpr 변수 캡처 시 컴파일 타임 사용 불가
- 재귀는
std::function필요 (C++17) - 예외, static 변수 사용 불가
다음 단계
- C++ constexpr 함수
- C++ if constexpr
- C++ Template Lambda
관련 글
- C++20 consteval 완벽 가이드 | 컴파일 타임 전용 함수
- C++ constexpr 함수 |
- C++ if constexpr |
- C++ any |
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트 | 현업에서 자주 쓰는 한눈에 보기