constexpr 이해하기 - 컴파일 타임 vs 런타임 평가
constexpr
0. 서문
솔직히 말씀드리면 - constexpr은 밤잠을 설치게 만들었습니다! 너무 혼란스러워서 더 이상 참을 수 없어 이 종합 가이드를 작성하기로 했습니다.
constexpr은 Modern C++ (C++11 이상)에서 지원됩니다. constexpr의 사양은 STL 버전이 발전함에 따라 계속 진화하고 있습니다.
다양한 강의 자료와 사용 사례를 보니, 모든 사람이 다르게 사용하고 다르게 이해하는 것 같습니다. constexpr의 원저자는 분명히 명확한 의도를 가지고 있었겠지만, 지금은 초기 개념과는 꽤 다르게 느껴집니다.
하지만 모든 사람이 공통적으로 묻는 한 가지가 있습니다:
“const와 어떻게 다른가요?”
const는 단순히 상수입니다 - 컴파일되면 런타임 중에 데이터가 불변이 됩니다.
constexpr도 비슷한 의도를 가지고 있습니다. 프로그래머들은 변수나 함수가 컴파일 타임에 결정되도록 하기 위해 사용합니다.
하지만 여기서 constexpr이 약간 모호해집니다: constexpr로 선언된 변수나 함수는 컴파일 중에 결정될 수도 있고 런타임 중에 결정될 수도 있습니다!
cpp reference c++1x constexpr
이 모호함을 탐구하기 위해 몇 가지 예제를 살펴보겠습니다…
1. 일반적인 예제
#include <iostream>
int fibonacci(int n){
if (n >= 2)
return fibonacci(n-1) + fibonacci(n-2);
else
return n;
}
int main(){
std::cout << fibonacci(10) << '\n';
}
간단한 피보나치 수열 계산기입니다.
이 코드는 컴파일 후 런타임에 값을 계산합니다.
$ /usr/bin/time ./fibonacci
102334155
0.56user 0.00system 0:00.56elapsed 100%CPU (0avgtext+0avgdata 3324maxresident)k
0inputs+0outputs (0major+126minor)pagefaults 0swaps
값이 런타임에 계산되므로 꽤 시간이 걸립니다.
2. 템플릿 접근법
컴파일 타임 계산의 효율성을 원한다면? C++ 템플릿 메타 프로그래밍을 시도해봅시다:
#include <iostream>
template <int N>
struct fibonacci
{
static int64_t const value = fibonacci<N-1>::value + fibonacci<N-2>::value;
};
template<>
struct fibonacci<0>
{
static int64_t const value = 0;
};
template<>
struct fibonacci<1>
{
static int64_t const value = 1;
};
int main(){
std::cout << fibonacci<40>::value << '\n';
}
C++ 템플릿 기술을 사용하면, 컴파일러가 내부적으로 코드를 생성하고 컴파일 타임에 작업을 수행합니다.
따라서 fibonacci<40>::value는 컴파일 타임에 결정됩니다.
$ /usr/bin/time ./fibonacci_template
102334155
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 3388maxresident)k
0inputs+0outputs (0major+125minor)pagefaults 0swaps
값이 컴파일 타임에 계산되었으므로 실행 시간이 매우 빠릅니다!
3. constexpr
그렇다면 constexpr을 사용하면 어떻게 될까요?
#include <iostream>
constexpr int fibonacci(int n){
return n>=2 ? fibonacci(n-1) + fibonacci(n-2): n;
}
template<int N>
struct constN{
constN(){ std::cout << N << '\n';}
};
int main(){
constN<fibonacci(40)> a; // 컴파일 타임
//std::cout << fibonacci(40) << '\n'; //런타임
}
$ /usr/bin/time ./fibonacci_constexpr
102334155
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 3412maxresident)k
0inputs+0outputs (0major+125minor)pagefaults 0swaps
다시 한 번, 값이 컴파일 타임에 계산되었기 때문에 빠른 실행!
흥미로운 부분은 여기입니다: constexpr은 두 가지 형태로 모두 사용할 수 있습니다.
위의 같은 코드에서 주석 처리된 줄을 주석 해제하면, 값이 런타임에 결정되고 훨씬 더 오래 걸립니다:
$ /usr/bin/time ./fibonacci_constexpr
102334155
0.55user 0.00system 0:00.55elapsed 99%CPU (0avgtext+0avgdata 3376maxresident)k
0inputs+0outputs (0major+125minor)pagefaults 0swaps
4. 결론
우리가 본 것처럼, constexpr은 컴파일 타임과 런타임 모두에서 사용할 수 있습니다.
컴파일 중에 constexpr 요구사항이 충족되지 않으면 자동으로 런타임 계산으로 대체됩니다.
constexpr을 사용하면 두 가지 장점을 모두 얻을 수 있습니다:
- 효율성을 위해 컴파일 타임에 결정된 값을 가질 수 있습니다
- 브레이크포인트에서 런타임 값을 검사하기 위해 디버거를 사용할 수 있습니다
제 결론: constexpr은 값이 컴파일 타임에 결정될 수 있다는 프로그래머의 의도를 보여주는 기술이지만, 반드시 그렇게 될 것을 보장하지는 않습니다.
마치 “컴파일러야, 이걸 컴파일 타임에 알아낼 수 있으면 그렇게 해줘. 못하겠으면 괜찮아 - 그냥 런타임에 계산해도 돼.”라고 말하는 것과 같습니다.
꽤 멋지죠? 🚀