-----------------
title : 씹어먹는 C++ - <16 - 2. constexpr 와 함께라면 컴파일 타임 상수는 문제없어>
cat_title : 16 - 2. constexpr 와 함께라면 컴파일 타임 상수는 문제없어
path : /C++
publish_date : 2019-08-18
next_page : 294
chapter : 컴파일 타임 상수 constexpr
-----------------

이번 강좌에서는

* 컴파일 타임 상수
* `constexpr` 객체, `constexpr` 함수, `constexpr` 생성자
* 리터럴 타입

에 대해 다룹니다.

![](/img/ChewingCpplogo.png)

안녕하세요 여러분! 이번 강좌에서는 `C++ 11` 에서 새롭게 도입된 `constexpr` 키워드에 대해 알아보도록 하겠습니다. `constexpr` 키워드는 객체나 함수 앞에 붙일 수 있는 키워드로, **해당 객체나 함수의 리턴값을 컴파일 타임에 값을 알 수 있다** 라는 의미를 전달하게 됩니다.

컴파일러가 컴파일 타임에 어떠한 식의 값을 결정할 수 있다면 해당 식을 **상수식 (Constant expression)** 이라고 표현합니다. 그리고 이러한 상수식들 중에서 값이 정수인 것을 **정수 상수식(Integral constant expression)** 이라고 하게 되는데, 정수 상수식들은 매우 쓰임새가 많습니다.

예를 들어서

```cpp-formatted
int arr[size];
```

위 배열 선언식이 컴파일 되기 위해서는 `size` 가 정수 상수식이여야 하고,

```cpp-formatted
template <int N>
struct A {
  int operator()() { return N; }
};
A<number> a;
```

템플릿 타입 인자의 경우도 마찬가지로 `number` 가 정수 상수식이여야만 합니다. 그 외에도,

```cpp-formatted
enum A { a = number, b, c };
```

`enum` 에서 값을 지정해줄 때에 오는 `number` 역시 정수 상수식이여만 합니다. 이처럼 C++ 언어 상 정수 상수식이 등장하는 곳은 매우 많습니다.

### constexpr

`constexpr` 은 앞서 말한 대로, 어떠한 식이 **상수식** 이라고 명시해주는 키워드 입니다. 만일, 객체의 정의에 `constexpr` 이 오게 된다면, 해당 객체는 어떠한 상수식에도 사용될 수 있습니다. 아래 예시를 보실까요.

```cpp-formatted
#include <iostream>

template <int N>
struct A {
  int operator()() { return N; }
};

int main() {
  constexpr int size = 3;
  int arr[size];  // Good!

  constexpr int N = 10;
  A<N> a;  // Good!
  std::cout << a() << std::endl;

  constexpr int number = 3;
  enum B { x = number, y, z };  // Good!
  std::cout << B::x << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
10
3
```

와 같이 잘 나옵니다.

`constexpr` 은 언뜻 보기에 `const` 와 큰 차이가 없어 보입니다. `constexpr` 로 정의된 변수들도 마찬가지로 상수이므로 수정할 수 없기 때문이죠. 하지만 둘은 큰 차이가 있습니다.

### constexpr vs const

`const` 로 정의된 상수들은 굳이 컴파일 타임에 그 값을 알 필요가 없습니다. 예를 들어서;

```cpp-formatted
int a;

// Do something...

const int b = a;
```

위 코드를 볼 때 b 의 값을 컴파일 타임에 알 수 는 없지만, b 의 값을 지정해주면 바꿀 수 없다는 점은 확실합니다.

반면에

```cpp-formatted
int a;

// Do something...

constexpr int b = a;  // ??
```

반면에 `constexpr` 변수의 경우 반드시 오른쪽에 다른 상수식이 와야 합니다. 하지만 컴파일러 입장에서 컴파일 타임 시에 `a` 가 뭐가 올 지 알 수 없습니다. 따라서 위 코드는 컴파일 오류가 됩니다. 정리하자면, **`constexpr` 은 항상 `const` 이지만, `const` 는 `constexpr` 이 아닙니다!**

여담으로 `const` 객체가 만일 상수식으로 초기화 되었다 하더라도 컴파일러에 따라 이를 런타임에 초기화 할지, 컴파일에 초기화할지 다를 수 있습니다. 예컨대

```cpp-formatted
const int i = 3;
```

위의 경우 `i` 는 컴파일 타임에 초기화될 수 도, 런타임에 초기화될 수 도 있습니다. **따라서 컴파일 타임에 상수를 확실히 사용하고 싶다면 `constexpr` 키워드를 꼭 사용해야 합니다.**

### constexpr 함수

앞서 `constexpr` 로 객체를 선언한다면 해당 객체는 컴파일 타임 상수로 정의된다고 하였습니다. 그렇다면 컴파일 타임 상수인 객체들을 만들어내는 함수를 정의할 수 는 없을까요?

`constexpr` 키워드가 등장하기 이전에는 컴파일 타임 상수인 객체를 만드는 함수를 작성하는 것이 불가능 하였습니다. 예를 들어서;

```cpp-formatted
#include <iostream>

int factorial(int N) {
  int total = 1;
  for (int i = 1; i <= N; i++) {
    total *= i;
  }
  return total;
}

template <int N>
struct A {
  int operator()() { return N; }
};

int main() { A<factorial(5)> a; }
```

컴파일 하였다면

```compiler-warning
test2.cc: In function ‘int main()’:
test2.cc:17:14: error: call to non-constexpr function ‘int factorial(int)’
   A<factorial(5)> a;
     ~~~~~~~~~^~~
test2.cc:17:14: error: call to non-constexpr function ‘int factorial(int)’
test2.cc:17:17: note: in template argument for type ‘int’ 
   A<factorial(5)> a;
                 ^
```

와 같은 오류가 발생하게 됩니다. 왜냐하면 `factorial(5)` 는 컴파일 타임 상수가 아니기 때문이지요. 물론 우리는 똑똑하기 때문에 `factorial(5)` 를 컴파일 타임에 계산해서 그냥 `A<120> a;` 로 컴파일 해도 된다는 것을 알고 있지만, 컴파일러는 그렇지 않습니다.

따라서 이와 같은 문제를 해결하기 위해 기존에는 난해한 템플릿 메타프로그래밍을 사용해야했습니다. 예를 들어서 위 `factorial` 함수를 TMP 방식으로 접근한 코드를 살펴봅시다.

```cpp-formatted
#include <iostream>

template <int N>
struct Factorial {
  static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
  static const int value = 1;
};

template <int N>
struct A {
  int operator()() { return N; }
};

int main() {
  // 컴파일 타임에 값이 결정되므로 템플릿 인자로 사용 가능!
  A<Factorial<10>::value> a;

  std::cout << a() << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
3628800
```

와 같이 잘 나옵니다.

```cpp-formatted
template <int N>
struct Factorial {
  static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
  static const int value = 1;
};
```

아무래도 이전에 템플릿 메타프로그래밍 강좌를 보신 분들은 위 코드가 무엇을 하는지 단박에 이해하실 수 있을 것이라 생각합니다. N 부터 1 까지의 곱을 TMP 의 형태로 수행하게 됩니다.

```cpp-formatted
// 컴파일 타임에 값이 결정되므로 템플릿 인자로 사용 가능!
A<Factorial<10>::value> a;
```

그렇다면 우리의 `Factorial` 클래스를 통해 계산한 `10!` 의 값을 사용해서 배열을 정의할 수 도 있습니다. 이 경우 위 코드는 `A<3628800> a` 와 동일합니다.

위와 같이 배열의 크기를 정의할 수 있는 이유는 `Factorial<10>::value` 의 값을 컴파일러가 컴파일 타임에 알아낼 수 있기 때문입니다.

하지만, 위 `Factorial` 클래스처럼 간단한 경우를 빼면 사실 TMP 를 이용해서 구현된 코드는 딱히 이해하기 쉽지 않습니다. 왜냐하면 템플릿의 특성상 조건문들은 대개 템플릿 특수화를 통해 구현되고, 반복문들은 재귀 호출의 형태로 구현되어야 하기 때문에 복잡하기 때문입니다.

하지만 함수의 리턴 타입에 `constexpr` 을 추가한다면 조건이 맞을 때, 해당 함수의 리턴값을 컴파일 타임 상수로 만들어버릴 수 있습니다.

그럼 아래 코드를 살펴보겠습니다.

```cpp-formatted
#include <iostream>

constexpr int Factorial(int n) {
  int total = 1;
  for (int i = 1; i <= n; i++) {
    total *= i;
  }
  return total;
}

template <int N>
struct A {
  int operator()() { return N; }
};

int main() {
  A<Factorial(10)> a;

  std::cout << a() << std::endl;
}
```

성공적으로 컴파일 하였다면

```cpp-formatted
3628800
```

와 같이 잘 나옵니다! 놀랍게도 `Factorial(10)` 이 컴파일 타임에 계산되어서 클래스 A 의 템플릿 인자로 들어가게 되었습니다.

```cpp-formatted
constexpr int Factorial(int n) {
  int total = 1;
  for (int i = 1; i <= n; i++) {
    total *= i;
  }
  return total;
}
```

위 `constexpr` 함수를 살펴보면 그냥 일반 함수와는 다를 바가 없습니다. 사실 `C++ 11` 에 `constexpr` 이 처음 도입되었을 때에는 `constexpr` 함수에는 여러 제약 조건이 많았습니다. 예를 들어서 함수 내부에서 변수들을 정의할 수 없고, `return` 문은 딱 하나만 있어야만 했습니다.

하지만 `C++ 14` 부터 위와 같은 제약 조건들이 완화되어서 아래와 제약 조건들 빼고는 모두 `constexpr` 함수 내부에서 수행할 수 있습니다.

* `goto` 문 사용
* 예외 처리 (`try` 문; C++ 20 부터 가능하게 바뀌었습니다.)
* **리터럴 타입**이 아닌 변수의 정의
* 초기화 되지 않는 변수의 정의
* 실행 중간에 `constexpr` 이 아닌 함수를 호출하게 됨

따라서 위와 같은 작업들을 하지 않는 이상 `constexpr` 키워드를 함수에 붙일 수 있게 됩니다. 만일 조건을 만족하지 않는 작업을 함수 내에서 하게 된다면 컴파일 타임 오류가 발생하게 됩니다. 예를 들어서

```cpp-formatted
int not_constexpr(int x) { return x++; }
constexpr int Factorial(int n) {
  int total = 1;
  for (int i = 1; i <= n; i++) {
    total *= i;
  }

  not_constexpr(total);
  return total;
}
```

위 경우 중간에 `constexpr` 함수가 아닌 함수를 호출하게 되므로

```compiler-warning
test2.cc: In function ‘constexpr int Factorial(int)’:
test2.cc:28:16: error: call to non-constexpr function ‘int not_constexpr(int)’
   not_constexpr(total);
   ~~~~~~~~~~~~~^~~~~~~
```

위와 같은 오류가 발생하게 됩니다.

성공적으로 `constexpr` 함수를 정의하였다면 이를 이용해서 `constexpr` 상수들을 생성할 수 있습니다. `constexpr` 함수에 인자로 컴파일 타임 상수들을 전달하면, 그 반환값 역시 컴파일 타임 상수가 됩니다. 우리의 사용 예시도 마찬가지로

```cpp-formatted
A<Factorial(10)> a;
```

위 처럼 컴파일 타임 상수인 `10` 을 전달하였기 때문에 `Factorial(10)` 의 반환값은 컴파일 타임 상수가 되어서 위처럼 템플릿 인자로 전달 가능하게 됩니다. 당연하게도 `constexpr` 으로 정의된 상수들 역시 컴파일 타임 상수 이므로;

```cpp-formatted
constexpr int ten = 10;
A<Factorial(ten)> a;
```

위 역시 마찬가지로 동작합니다. 그렇다면 `constexpr` 함수는 컴파일 타임 상수들만 인자로 받을 수 있는 것일까요? 아닙니다! `constexpr` 함수에 인자로 컴파일 타임 상수가 아닌 값을 전달하였다면 그냥 일반 함수 처럼 동작하게 됩니다.

```cpp-formatted
#include <iostream>

constexpr int Factorial(int n) {
  int total = 1;
  for (int i = 1; i <= n; i++) {
    total *= i;
  }
  return total;
}

int main() {
  int num;
  std::cin >> num;
  std::cout << Factorial(num) << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
5
120
```

와 같이 잘 실행됩니다. 위 경우 `Factorial` 에 컴파일 타임 상수가 아닌 일반 값을 전달하였지만, 컴파일 타입이 아닌 런타임에 `Factorial` 이 실행되어서 잘 작동합니다.

따라서 `constexpr` 을 함수에 붙일 수 있다면 붙여주는 것이 좋습니다. 왜냐하면 `constexpr` 처럼 동작하지 못한다면 그냥 일반 함수처럼 동작할 테이고, 컴파일 타임 상수를 생성할 수 있는 상황이라면 간단히 이용할 수 있기 때문이지요.

### 리터럴 타입?

앞서 `constexpr` 함수 내부에서 불가능한 작업으로 리터럴(Literal) 타입이 아닌 변수의 정의라고 이야기 하였습니다. 리터럴 타입은 쉽게 생각하면 컴파일러가 컴파일 타임에 정의할 수 있는 타입이라고 생각하시면 됩니다. C++ 에서 정의하는 바로는;

* `void` 형
* 스칼라 타입 (`char, int, bool, long, float, double`) 등등
* 레퍼런스 타입
* 리터럴 타입의 배열
* 혹은 아래 조건들을 만족하는 타입
  * 디폴트 소멸자를 가지고
  * 다음 중 하나를 만족하는 타입
    * 람다 함수
    * `Arggregate` 타입 (사용자 정의 생성자, 소멸자가 없으며 모든 데이터 멤버들이 `public`)
      쉽게 말해 `pair` 같은 애들을 이야기함
    * **`constexpr` 생성자를 가지며 복사 및 이동 생성자가 없음**

들을 리터럴 타입이라 의미하며 해당 객체들만이 `constexpr` 로 선언되던지 `constexpr` 함수 내부에서 사용될 수 있습니다. 이전에는 리터럴 타입으로 정의되어 있는 것들이 매우 한정적이였는데 (대부분 스칼라 타입), `C++ 14` 부터 `constexpr` 생성자를 지원함으로써 사용자들이 리터럴 타입들을 직접 만들 수 있게 되었습니다.

그렇다면 `constexpr` 생성자를 어떻게 사용하는지 살펴보겠습니다.

### constexpr 생성자

`constexpr` 로 생성자의 경우 일반적인 `constexpr` 함수에서 적용되는 제약조건들이 모두 적용됩니다. 또한 `constexpr` 생성자의 인자들은 반드시 리터럴 타입이여야만 하고, 해당 클래스는 다른 클래스를 가상 상속 받을 수 없습니다.

예를 들어서 아래와 같은 클래스를 생각해봅시다.

```cpp-formatted
class Vector {
 public:
  constexpr Vector(int x, int y) : x_(x), y_(y) {}

  constexpr int x() const { return x_; }
  constexpr int y() const { return y_; }

 private:
  int x_;
  int y_;
};
```

위 `Vector` 클래스는 벡터를 나타내는 클래스 입니다 (std::vector 가 아닙니다!). `Vector` 의 생성자는 리터럴인 `int` 두 개를 인자로 받고 있습니다. 따라서 이는 적합한 `constexpr` 생성자가 되겠지요.

```cpp-formatted
constexpr int x() const { return x_; }
constexpr int y() const { return y_; }
```

마찬가지로 두 멤버 변수를 접근하는 함수 역시 `constexpr` 로 정의해주었습니다. 따라서 `x()` 와 `y()` 역시 `constexpr` 함수 내부에서 사용할 수 있게 됩니다.

그렇다면 실제 컴파일 시에 어떻게 작동하는지 살펴봅시다.

```cpp-formatted
#include <iostream>

class Vector {
 public:
  constexpr Vector(int x, int y) : x_(x), y_(y) {}

  constexpr int x() const { return x_; }
  constexpr int y() const { return y_; }

 private:
  int x_;
  int y_;
};

constexpr Vector AddVec(const Vector& v1, const Vector& v2) {
  return {v1.x() + v2.x(), v1.y() + v2.y()};
}

template <int N>
struct A {
  int operator()() { return N; }
};

int main() {
  constexpr Vector v1{1, 2};
  constexpr Vector v2{2, 3};

  // constexpr 객체의 constexpr 멤버 함수는 역시 constexpr!
  A<v1.x()> a;
  std::cout << a() << std::endl;

  // AddVec 역시 constexpr 을 리턴한다.
  A<AddVec(v1, v2).x()> b;
  std::cout << b() << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
1
3
```

와 같이 잘 나옵니다.

```cpp-formatted
constexpr Vector v1{1, 2};
constexpr Vector v2{2, 3};
```

먼저 위 처럼 우리가 만든 클래스인 `Vector` 를 `constexpr` 로 선언할 수 있었습니다. 왜냐하면 `constexpr` 로 생성자를 만들었기 때문이지요.

```cpp-formatted
A<v1.x()> a;
```

그리고 `v1` 의 `constexpr` 멤버 함수인 `x` 를 호출하였는데, `x` 역시 `constexpr` 함수이므로 위 코드는 결국 `A<1> a` 와 다름이 없게 됩니다. 만일 `v1` 이나 `x` 가 하나라도 `constexpr` 이 아니라면 위 코드는 컴파일 되지 않습니다. **constexpr 객체의 constexpr 멤버 함수만이 constexpr 을 줍니다!**

```cpp-formatted
// AddVec 역시 constexpr 을 리턴한다.
A<AddVec(v1, v2).x()> b;
```

그렇다면 우리의 `AddVec` 함수는 어떨까요? 마찬가지로 `v1` 과 `v2` 를 인자로 받아서 `constexpr` 객체를 리턴하게 됩니다. 이것이 가능한 이유는 `AddVec` 이 `constexpr` 함수 이고, `Vector` 가 리터럴 타입이여서 그렇겠지요.

### if constexpr

만약에 타입에 따라 형태가 달라지는 함수를 짜고 싶다면 어떻게 하시나요? 예를 들어서 `get_value` 라는 함수가 있는데, 이 함수는 인자가 포인터 타입이면 `*` 을 한 것을 리턴하고 아니면 그냥 원래의 인자를 리턴하는 함수 입니다.

템플릿 타입 유추를 이용하면 해당 함수는 다음과 같이 작성할 수 있습니다.

```cpp-formatted
#include <iostream>

template <typename T>
void show_value(T t) {
  std::cout << "포인터가 아니다 : " << t << std::endl;
}

template <typename T>
void show_value(T* t) {
  std::cout << "포인터 이다 : " << *t << std::endl;
}

int main() {
  int x = 3;
  show_value(x);

  int* p = &x;
  show_value(p);
}
```

성공적으로 컴파일 하였다면

```exec
포인터가 아니다 : 3
포인터 이다 : 3
```

와 같이 잘 나옵니다. 아주 좋습니다. 하지만 문제는 1) `show_value` 함수가 정확히 어떠한 형태의 `T` 를 요구하는지 한 눈에 파악하기 힘들고 2) 같은 함수를 두 번 써야 한다는 점입니다.

C++ 표준 라이브러리의 `<type_traits>` 에서는 여러가지 템플릿 함수들을 제공하는데, 이들 중 해당 타입이 포인터 인지 아닌지 확인하는 함수도 있습니다. 이를 사용해서 한 번 구성해보겠습니다.

```cpp-formatted
#include <iostream>
#include <type_traits>

template <typename T>
void show_value(T t) {
  if (std::is_pointer<T>::value) {
    std::cout << "포인터 이다 : " << *t << std::endl;
  } else {
    std::cout << "포인터가 아니다 : " << t << std::endl;
  }
}

int main() {
  int x = 3;
  show_value(x);

  int* p = &x;
  show_value(p);
}
```

컴파일 하였다면

```compiler-warning
test2.cc: In instantiation of ‘void show_value(T) [with T = int]’:
test2.cc:15:15:   required from here
test2.cc:6:43: error: invalid type argument of unary ‘*’ (have ‘int’)
     std::cout << "포인터 이다 : " << *t << std::endl;
                                           ^~
```

와 같이 오류가 발생합니다. 일단 `is_pointer` 부터 살펴봅시다.

```cpp-formatted
if (std::is_pointer<T>::value) {
```

`std::is_pointer` 는 전달한 인자 `T` 가 포인터라면 `value` 가 `true` 가 되고, 포인터가 아니면 `false` 가 되는 템플릿 메타 함수 입니다. 따라서 만일 `T` 가 포인터라면 `*t` 를 출력하고 아니면 `t` 를 출력하겠지요.

하지만 문제는 템플릿이 인스턴스화 되면서 생성되는 코드에 컴파일이 불가능한 부분이 발생된다는 것입니다. `show_value(x)` 를 하게 된다면 생성되는 코드는

```cpp-formatted
void show_value(int t) {
  if (std::is_pointer<int>::value) {
    std::cout << "포인터 이다 : " << *t << std::endl;
  } else {
    std::cout << "포인터가 아니다 : " << t << std::endl;
  }
}
```

가 되므로 `int` 타입인 `t` 에 `*` 연산자가 붙게 됩니다. 따라서 위 `if` 문은 절대로 실행되지 않음에도 불구하고 컴파일 되지 않기 때문에 오류가 발생한 것이지요.

하지만 이 문제는 `if constexpr` 을 도입하면 깔끔히 해결됩니다.

```cpp-formatted
#include <iostream>
#include <type_traits>

template <typename T>
void show_value(T t) {
  if constexpr (std::is_pointer<T>::value) {
    std::cout << "포인터 이다 : " << *t << std::endl;
  } else {
    std::cout << "포인터가 아니다 : " << t << std::endl;
  }
}

int main() {
  int x = 3;
  show_value(x);

  int* p = &x;
  show_value(p);
}
```

성공적으로 컴파일 하였다면

```exec
포인터가 아니다 : 3
포인터 이다 : 3
```

와 같이 잘 나옵니다. `if constexpr` 은 조건이 반드시 `bool` 로 타입 변환될 수 있어야 하는 컴파일 타임 상수식이어야만 합니다. 그 대신, `if constexpr` 이 참이라면 `else` 에 해당하는 문장은 **컴파일 되지 않고 (완전히 무시)** 마찬가지로 `if constexpr` 이 거짓이라면 `else` 에 해당 하는 부분만 컴파일 됩니다.

따라서 위 경우 `std::is_pointer<int>::value` 는 거짓 이므로 아예 `*t` 자체가 컴파일 되지 않습니다. 덕분에 컴파일 오류는 발생하지 않습니다.

참고로 `std::is_pointer<T>::value` 가 거추장스럽다면 그냥 `std::is_pointer_v<T>` 만 써도 됩니다. `std::is_pointer_v<T>` 는 아래와 같이 정의되어 있습니다.

```cpp-formatted
template <class T>
inline constexpr bool is_pointer_v = is_pointer<T>::value;
```

(위 코드가 무엇을 뜻하는지 이해 하실 수 있겠죠?)

그렇다면 원래 함수는

```cpp-formatted
template <typename T>
void show_value(T t) {
  if constexpr (std::is_pointer_v<T>) {
    std::cout << "포인터 이다 : " << *t << std::endl;
  } else {
    std::cout << "포인터가 아니다 : " << t << std::endl;
  }
}
```

로 좀더 깔끔하게 바뀝니다.

### C++ 20

`C++ 20` 은 아직 나오지 않았지만, 추가될 기능들 중에 `constexpr vector (!!)` 와 `constexpr string (!!!)` 이 있습니다.\sidenote{참고로 string_view 는 이미 constexpr 입니다.} 이를 위해서 `constexpr new` 와 `constexpr` 소멸자가 추가되었다고 하니 `constexpr` 은 좀 더 많은 곳에서 사용될 것 같습니다. 심지어 디폴트로 함수를 그냥 `constexpr` 로 만들어버리자는 이야기도 나오고 있고 말이지요.

C++ 는 정말 끊임없이 발전하고 있습니다. 덕분에 배워야 할 것들도 정말 끊임 없는 것 같네요..

그렇다면 이번 강좌는 여기에서 마치도록 하겠습니다. 다음 강좌에서는 C++ 의 여러가지 라이브러리에 대해 소개하는 시간을 갖도록 하겠습니다.

```lec-summary
`constexpr` 을 통해 컴파일 타임 상수인 객체를 선언할 수 있다. 
const 와 `constexpr` 은 다르다. `const` 는 컴파일 타임에 상수일 필요가 없다! (`const` 인 애들 중에서 `constexpr` 이 있다고 생각하면 된다)
`constexpr` 로 정의된 함수는 인자로 리터럴을 전달하였을 때 컴파일 타임 상수를 리턴한다.
`constexpr` 생성자를 가진 클래스는 `constexpr` 객체를 생성할 수 있다.
```

##@ chewing-cpp-end