1. constexpr
기존의 const 보다 훨씬 더 상수성에 충실하며, 컴파일 타임 함수를 이용한 성능 향상 등 충분히 깊게 이해할만한 가치가 있는 녀석이라 할 수 있으니, 확실히 이해하고 활용할 수 있는 것이 중요하다.
C++11부터 지원되는 한정자 constexpr는 일반화된 상수 표현식(Generalized constant expression)을 사용할 수 있게 해주며, 일반화된 상수 표현식을 통해 변수나 함수, 생성자 함수에 대하여 컴파일 타임에 평가될 수 있도록 처리해 줄 수 있다.
(C++17부터는 람다 함수에도 constexpr 키워드 사용이 가능하다)
constexpr 변수 또는 함수의 반환값은 반드시 LiteralType이어야 하며, LiteralType은 컴파일 타임에 해당 레이아웃이 결정될 수 있는 타입을 의미한다. 다음은 리터럴 타입들의 유형이다.
- void
- 스칼라 값
- 참조
- void, 스칼라 값, 참조의 배열
- trivial 소멸자 및 이동 또는 복사 생성자가 아닌 constexpr 생성자를 포함하는 클래스. 또한 해당 비정적 데이터 멤버 및 기본 클래스가 모두 리터럴 타입이고 volatile이 아니어야 함
코드 작업 중 해당 타입이 리터럴 타입인지 확인하고 싶을 땐, std::is_literal_type을 사용하면 된다.
1) 변수에서의 사용
const와 constexpr의 주요 차이점은 const 변수의 초기화를 런타임까지 지연시킬 수 있는 반면, constexpr 변수는 반드시 컴파일 타임에 초기화가 되어 있어야 한다.
초기화가 안 되었거나, 상수가 아닌 값으로 초기화 시도시 컴파일이 되지 않는다.
constexpr float x = 42.f; // OK
constexpr float y { 108.f }; // OK
constexpr int i; // error C2737: 'i': 'constexpr' 개체를 초기화해야 합니다.
int j = 0;
constexpr int k = j + 1; // error C2131 : 식이 상수로 계산되지 않았습니다.
변수에 constexpr 사용시 const 한정자를 암시한다.
2) 함수에서의 사용
constexpr을 함수 반환값에 사용할 때는 다음의 제약들이 따른다.
- 가상으로 재정의된 함수가 아니어야 한다.
- 반환값의 타입은 반드시 LiteralType이어야 한다.
함수에 constexpr을 붙일 경우 inline을 암시한다.
즉, 컴파일 타임에 평가하기 때문이며, inline 함수들과 같이 컴파일된다.
C++11에서는 함수 본문에 지역변수를 둘 수 없고, 하나의 반환 표현식만이 와야 하는 제약이 있었으나, C++14부터는 이러한 제약이 사라졌다.
// C++11/14 모두 가능
constexpr int factorial(int n)
{
// 지역 변수 없음
// 하나의 반환문
return n <= 1 ? 1 : (n * factorial(n - 1));
}
// C++11에서는 컴파일 에러 발생
// C++14부터 가능
constexpr int factorial(int n)
{
// 지역 변수
int result = 0;
// 여러 개의 반환문
if (n <= 1)
result = 1;
else
result = n * factorial(n - 1);
return result;
}
constexpr 함수는 컴파일러에게 가능하다면, 상수시간에 컴파일해 달라고 요청하는 것이지만 상황에 따라 컴파일 타임에 미리 실행될 수도 있고, 그렇지 못하고 런타임에 실행될 수도 있다.
(마치 inline 키워드가 그러하듯이 말이다)
constexpr의 함수 인자들이 constexpr 규칙에 부합하지 못하는 경우엔 컴파일 타임에 실행되지 못하고 런타임에 실행된다.
런타임 실행 여부는 여러 가지 방식으로 검증해 볼 수 있다.
- breakpoint 걸어서 중단되는지 확인
- 아래 예제의 constN 같은 테스팅 템플릿 작성
- 정수일 경우 배열의 dimension으로 작성
// 템플릿 인자 N이 컴파일 타임 상수인지 테스트하기 위한 템플릿 구조체
template<int n>
struct constN
{
constN() { std::cout << N << '\n'; }
};
constexpr int factorial(int n)
{
return n <= 1 ? 1 : (n * factorial(n - 1));
}
int main()
{
// 4는 리터럴 타입이므로 상수 타임에 컴파일 성공
constN<factorial(4)> out1;
// ab는 4의 값을 가지지만, 리터럴 타입이 아니므로 컴파일 에러 발생
// error C2975: 'N': 'constN'의 템플릿 인수가 잘못되었습니다. 컴파일 타임 상수 식이 필요합니다.
int ab = 4;
constN<factorial(ab)> out2;
// 리터럴 타입이 아니므로, 이 함수는 런타임에 실행된다.
// 하지만 정상 동작한다.
int cd = factorial(ab);
return 0;
}
25라인에서 factorial 함수의 인자가 constexpr로 평가될 수 없기에, 함수이지만 런타임에 실행되는 것을 확인할 수 있다.
이처럼 constexpr 함수는 인자가 constexpr에 부합한지에 따라, 컴파일 타임 또는 런타임에 실행되기에 범용적으로 사용되는 함수이고, 실행의 복잡도가 낮지 않다면, 가급적 constexpr 키워드를 붙이는 것도 괜찮은 습관이 되지 않을까 생각한다.
위의 예제에서는 함수가 어느 시점에 실행되는지 여부를 살펴보았지만, 무작정 constexpr 키워드를 붙일 수 있는 것도 아니다.
함수가 절대 상수표현식으로써 평가받지 못하는 문맥을 가지는 경우엔 컴파일 에러가 발생한다.
constexpr int rand_short()
{
// 절대 상수화가 될 수 없는 문맥
// error C3615: constexpr 함수 'randshort'의 결과가 상수 식이면 안 됩니다.
return rand() % 65535;
}
컴파일 에러니까 함수 작성 후 바로 확인이 가능하기에 심각한 오류를 사전에 만나는 일은 없을 것이다.
3) 생성자 함수에서의 사용
LiteralType 클래스를 생성할 때 constexpr 생성자를 사용할 수 있다.
이 때의 제약은 다음과 같다.
- 모든 생성자 함수의 매개변수들 역시 LiteralType들이어야 한다.
- 어떤 클래스로부터 상속받지 않아야 한다
constexpr이 적용된 함수의 매개변수는 반드시 LiteralType이어야 한다고 했다.
이를 만족시키기 위해 constexpr 생성자 함수를 이용, LiteralType 클래스를 활용하는 예제를 살펴보도록 하자.
// 아래 CountLowercast 함수의 인자로 사용하기 위한 LiteralType class
class ConstString
{
private:
const char* p = null;
std::size_t sz = 0;
public:
template<std::size_t N>
constexpr ConstString(const char(&a)[N])
: p(a), sz(N - 1)
{
}
public:
constexpr char operator[](std::size_t n) const
{
return n < sz ? p[n] : throw std::out_of_range("");
}
public:
constexpr std::size_t size() const { return sz; }
};
// 소문자가 몇개인지 수를 세는 함수
constexpr std::size_t CountLowercase(ConstString s, std::size_t n = 0, std::size_t c = 0)
{
return n == s.size() ?
c :
s[n] >= 'a' && s[n] <= 'z' ?
CountLowercase(s, n+1, c+1) :
CountLowercase(s, n+1, c);
}
// 컴파일타임 상수를 테스트해 보기 위한 템플릿
template<int n>
struct ConstN
{
ConstN()
{
std::cout << n << '\n';
}
};
int main()
{
std::cout << "Number of lowercase letters in \"Hello, world!\" is ";
// "Hello, world!"가 ConstString으로 암시적 형반환되어, CountLowercase 함수의 인자로 넘어감.
// CountLowercase는 컴파일 타임에 평가되었고, 그 결과를 가지고 ConstN 역시 컴파일 타임에 결정됨.
ConstN<CountLowercase("Hello, world!")> out;
return 0;
}
2. 템플릿 메타 프로그래밍 vs constexpr
constexpr은 컴파일 타임에 평가되기 때문에 템플릿 메타 프로그래밍(템플릿 함수가 인스턴스화될 때 값 계산)과 비교될 수 있다.
예를 들어, 똑같이 배열의 크기나 enum 열거형의 값과 같은 곳에서 상수로써 사용이 가능하다.
// 템플릿 함수 방식의 Factorial
template <int N>
struct Template_Factorial
{
enum { value = N * Template_Factorial<N - 1>::value; }
};
template <>
struct Template_Factorial<0>
{
enum { value = 1; }
}
// constexpr 방식의 Factorial
constexpr int Constexpr_Factorial(int n)
{
return n <= 0 ? 1 : n * Constexpr_Factorial(n - 1);
}
// constexpr 함수의 결과를 enum의 값으로 사용 가능
enum FACTORIAL
{
first = Constexpr_Factorial(1),
second = Constexpr_Factorial(2),
third = Constexpr_Factorial(3),
};
위 예제에서 보듯이, 기존의 TMP에서 0과 같은 특수값을 사용하기 위해 템플릿 특수화를 했던 것에 비해, constexpr 함수는 훨씬 더 직관적인 방법을 제공할 수 있다.
위 예제에 피보나치까지 살짝 추가해, 조금 더 비교해 보면 기존 코드를 죄다 다시 작성하고 싶어질 것이다.
template<unsigned n>
struct Fibonacci
{
static const unsigned value = Fibonacci<n - 1>::value + Fibonacci<n - 2>::value;
};
template<>
struct Fibonacci<0>
{
static const unsigned value = 0;
};
template<>
struct Fibonacci<1>
{
static const unsigned value = 1;
};
int main()
{
return Fibonacci<5>::value;
}
///////////////////////////////////////////////////////////////////////////////////////////////
constexpr unsigned fibonacci(const unsigned x)
{
return x <= 1 ? 1 : fibonacci(x - 1) + fibonacci(x - 2);
}
int main()
{
return fibonacci(5);
}
출처 : http://egloos.zum.com/sweeper/v/3147813