const와 constexpr

const와 constexpr

2023년 7월 3일
c++11, c++14, c++17

const #

  1. 변수를 상수화해서 값 변경이 불가능하도록 한다.
    포인터 왼쪽에 const를 붙이면 해당 포인터로 접근하는 기능을 상수화 한다는 뜻이다.
    하지만 컴파일 타임에 상수일 필요는 없다.

     1const int a;        // Error! 초기화 하지않아서 에러 발생
     2
     3const int b = 10;   // 변수 b의 상수화
     4b = 20;             // Error! 상수의 변경 불가 
     5
     6int c = 10;
     7const int* d = &c;  // 포인터의 상수화
     8*d = 50;            // Error! 포인터에 접근해서 값 변경 불가
     9d = &a;             // 값 변경은 가능
    10
    11int* const e = &c;  // 변수의 상수화
    12e = &b;             // Error! 값 변경 불가
    
  2. 클래스의 멤버변수를 상수화 하면 생성자의 초기화리스트에서만 초기화가 가능하다.

    1class Test {
    2    const int data;
    3public:
    4    Test(int d) : data(d) {
    5        // data = d;    // Error! 값변경으로 인식함
    6    }
    7};
    
  3. 함수의 리턴값을 상수화
    레퍼런스나 포인터인 경우 리턴값을 const로 전달할 수 있다. 리턴받을 데이터를 const 형으로 선언해야 저장이 가능하다.

    1const int func() { int n = 10; return n; }
    2int a = func();         // const 아니여도 됨
    3
    4const int& func2() { int n = 20; return n; }
    5int& b = func2();       // Error! const int& 타입으로 받아야한다.
    6const int& c = func2(); // 성공
    
  4. 메서드를 상수화
    const 메서드는 클래스의 상태(멤버) 값을 변경시키지 않는다는 의미이다.

    1class Test {
    2    int a = 10;
    3public:
    4    void sett(int d) const {
    5        a = d;    // Error! 멤버변수를 수정할 수 없다.
    6    }
    7};
    

constexpr #

컴파일러에게 객체나 함수의 리턴값을 컴파일타임에 알 수 있다는 의미를 전달하기위해 c++11 부터 지원되는 키워드이다.
컴파일타임에 값을 알수있기 때문에 컴파일러가 계산해서 상수값을 집어넣어준다.
컴파일타임에 값을 알기 위해서는 아래 예시등에 쓰이는 상수식이여야 한다.

 1// 상수식 예시
 2// 1. 배열의 사이즈
 3int arr[size];
 4
 5// 2. 클래스의 템플릿으로 전달될 값
 6template <int N>
 7class A { ... }
 8A<number> a;
 9
10// 3. enum 에서 매핑할 값
11enum A { a = number, b, c };

constexpr vs const #

const는 그냥 변수를 수정 불가능하게 하는것이고, 컴파일타임에 값이 정해질 필요는 없다.
const는 실제 상수식이라 해도 컴파일타임에 초기화되지 않을 수 있는데, constexpr 키워드는 무조건 컴파일타임에 초기화하거나 에러가 발생한다.


c++11 이전 #

constexpr 키워드가 없을땐 TMP(템플릿 메타프로그래밍)을 이용해서 컴파일 시간에 함수를 실행해 최적화했다.

 1template <int N>
 2struct Factorial {
 3  static const int value = N * Factorial<N - 1>::value;
 4};
 5
 6template <>
 7struct Factorial<0> {     // 0이 들어왔을때 특수화
 8  static const int value = 1;
 9};
10
11template <int N>
12struct A {
13  int operator()() { return N; }
14};
15
16int main() {
17  // 컴파일 타임에 값이 결정되므로 템플릿 인자로 사용 가능!
18  A<Factorial<10>::value> a;
19}

c++11 constexpr #

  • 함수의 리턴과 매개변수는 리터럴(상수) 타입이여야 한다.
  • 함수는 하나의 return문을 가져야 하며 단일 표현식으로 작성되어야 한다.
    위의 조건을 만족시키면 constexpr 키워드를 붙일 수 있고, factorial 함수를 컴파일타임에 상수로 치환시킬 수 있다.
    하지만, 매개변수 n이 리터럴 타입인 경우만 상수로 치환되고, 리터럴 타입이 아닌경우 일반 함수처럼 런타임에 동작한다.

리터럴 타입: 컴파일 타임에 값이 정해져서 프로그램이 실행되는 동안 값이 변하지 않는 상수

 1// c++11
 2constexpr int factorial(int n) {
 3    return (n <= 1) ? 1 : (n * factorial(n - 1)); // 단일 return
 4}
 5
 6A<factorial(10)> a;     // constexpr 함수라 사용 가능
 7
 8int b;
 9cin >> b;
10factorial(b);           // b는 리터럴 타입이 아니라서 factorial의 constexpr이 무시된다. 

constexpr 생성자 #

컴파일 시간에 객체를 초기화할때 사용하는 값들을 미리 계산해서 런타임 초기화시간을 줄일 수 있다.
constexpr에 함수에서 적용되는 제약조건들과 동일하다

constexpr 생성자여도 constexpr 객체를 생성하는게 아니라면 일반 객체 생성자가 호출되며 생성된다.

1class Test {
2    int a = 10;
3    int b = 10;
4    int c;
5public:
6    constexpr Test(int n) : c(n) {    // 생성자에서라도 초기화 해줘야한다.
7        for (int i = 0; i < n; i++) a += i;
8    };
9};

a 의 값은 for문을 돌려서 초기화하는데, constexpr이라 생성자가 호출되지 않고 미리 계산된 값이 저장된다.

constexpr생성자

호출할 부모클래스의 생성자까지 constexpr이여야 constexpr 객체를 생성할 수 있게 된다.


c++14 constexpr #

기존의 constexpr은 단일표현식을 리턴하면서 제어구문(if, switch, for, while등)을 사용할 수 없었다.
c++14부터는 제어구문이나 변수선언 등이 가능해졌다.

아래의 경우가 포함되면 c++14에서도 constexpr이 불가능하다

  • goto
  • try~catch (c++20부터 추가됨)
  • 리터럴 타입이 아닌 변수 정의
  • 초기화되지 않는 변수의 정의
  • 실행 중간에 constexpr이 아닌 함수 호출

1constexpr int factorial(int n) {
2    int result = 1;
3    for (int i = 2; i <= n; ++i) {
4        result *= i;
5    }
6    return result;
7}

c++17 if constexpr #

컴파일 시간에 조건에 따라 코드를 포함할지 말지를 결정한다.
조건식이 상수식이여야 하고, 조건에따라 컴파일 이후 코드에 조건에 맞는 코드만 포함된다.

보통 템플릿 함수에서 템플릿 인자에 따라 런타임에 각각의 함수를 만들어내는데, 절대 실행되지 않는 코드가 컴파일이후에 포함되기 때문에 생겨난 문법이다.

전달받은 타입이 포인터인지, 아닌지를 확인하는 함수이다. std::is_pointer_v<T> 도 역시 상수식이며 템플릿함수 show_value는 컴파일 중 타입에 따라 show_value라는 함수의 오버로딩본이 만들어진다.

어차피 타입에 따라 if문 이전, 이후 둘중 하나의 코드만 실행되기 때문에 컴파일 타임에 나머지 코드는 제거해도 된다.

 1template<typename T>
 2void show_value(T t) {
 3    if constexpr (std::is_pointer_v<T>)
 4        std::cout << "포인터 이다 : ";
 5    else
 6        std::cout << "포인터가 아니다 : ";
 7}
 8
 9int a = 0;
10int* b = &a;
11show_value(a);
12show_value(b);   // 초기화된 값이여야 한다.

템플릿 함수라 어차피 show_value<int>(a), show_value<int *>(b)는 다른 주소의 함수인데, 서로 필요없는 코드가 중복되어 포함된 함수가 만들어진다. 호출되는함수주소

if constexpr을 사용하지 않은경우 #

비교구문, 조건이 참일때, 거짓일때 문자열을 출력하는 cout의 operator<< call 로직이 모두 포함되어 있다. 일반if

if constexpr을 사용한 경우 #

show_value<int *>(b) 함수에서는 조건식과 else 이후의 operator<< call이 포함되지 않고 바로 함수 에필로그가 실행된다. ifconstexpr

comments powered by Disqus