본문 바로가기

진리는어디에/C++

[C++] constexpr 조건문

들어가며

다른 프로그래밍 언어와 C++의 큰 차이점을 들자면 C++ 표준 위원회는 강박증이라고 생각될 정도로 컴파일 타임에 뭔가를 하는 것에 집착하는것 처럼 보인다.

오늘 포스트에서 다루어 볼 constexpr도 그 중의 하나로써, 이전 포스트 constexpr 상수, constexpr 함수 에서 constexpr을 이용해 컴파일 타임 상수와 컴파일 타임에 실행 되는 함수에 대해 살펴 본것에 이어 컴파일 타임에 조사 되는 if 문을 살펴 보고자 한다.

if constexpr

일반적인 조건문은 실행 시간에 조건식을 조사하는 반면에 constexpr 조건문은 컴파일 시간에 조건식을 조사한다. 이 때 if constexpr의 조건식에 들어가는 조건문은 반드시 컴파일 타임에 값을 결정할 수 있어야만 한다.

 

// 일반적인 if 문

int n = 0;

if (0 == n)
{
    // ...
}

// constexpr if 문
if constexpr (0 == n)
{
    // ...
}

위 코드는 if constexpr의 사용 문법을 보여 주고 있다. 간단하게 if 키워드와 조건식 사이에 constexpr 키워드를 넣어주는 것이 전부다.

하지만 위 코드의 조건식에서 평가되는 n은 컴파일 타임 상수가 아닌 런타임에 언제든 변경 될 수 있는 일반 변수이므로 컴파일 타임에 값을 결정할 수 없다. 그래서 위 코드를 컴파일하게 되면 에러가 발생한다.

다시 아래의 코드를 살펴 보도록 하자.

int n = 0;

if constexpr (sizeof(int) == 0) // sizeof는 컴파일 타임에 연산된다
{
    // ...
}

sizeof는 함수가 아닌 컴파일 타임에 타입의 크기를 알아내는 연산자다. 당연히 컴파일 타임에 값이 결정 되므로 constexpr의 조건식에 사용 가능하다.

위 조건식은 4바이트인 정수 타입의 크기를 0과 비교하고 있으므로 위의 코드는 컴파일 타임에 false로 판정되고 if constexpr 블록 내에 있는 코드들은 컴파일 되지 않는다.

constexpr 조건문의 활용

constexpr의 문법을 살펴 보았으므로, 이제 constexpr을 어디에 쓸 수 있는지 살펴 보도록 하자.

여러분은 아마도 항상 같은 값을 가지는 상수에 대해 조건식을 비교한다는 것이 무슨 의미가 있는지 궁금할 것이다. 사실 if constexpr문은 일반적인 프로그래밍에서는 사용 될 일이 거의 없다. 하지만 컴파일 타임에 값이 결정되고 코드가 생성되는 템플릿 프로그래밍에서는 템플릿 파라메터에 따라 다른 코드를 생성해 낼 수 있는 아주 강력한 도구가 된다.

템플릿 파라메터들은 컴파일 타임에 결정 되고, 이런 템플릿 파라메터들에 대한 조건식을 조사하여 값에 따라 다른 로직을 수행 할 수 있게 해주는 것이 바로 constexpr 조건문이다.

간단한 예를 들어 아래와 같이 템플릿 인자로 넘어오는 상수 인자에 따라 어떤 때는 new를 이용해 배열을 생성하고 어떤 때는 고정 사이즈의 배열 포인터를 대입한다고 가정해보자(억지스러운 예인 것은 알지만 활용 방법을 보여주기 위한 것이니 실용성에 대한 논란은 잠시 뒤로 미뤄 두자).

#include <iostream>

static int buff_1024[1024] = { 0 };
static int buff_2048[2048] = { 0 };

template <int N>
struct Buffer
{
    Buffer()
    {
        if constexpr (N <= 1024) // N의 크기에 따라 다른 버퍼를 할당
        {
            data = buff_1024;
        }
        else
        {
            data = buff_2048;
        }
    }

    int* data;
};

int main()
{
    Buffer<1024> buf1;
    Buffer<1024> buf2;
    Buffer<2048> buf3;

    std::cout << std::hex;
    std::cout << "Buff<1024>:" << buf1.data << std::endl;
    std::cout << "Buff<1024>:" << buf2.data << std::endl;
    std::cout << "Buff<2048>:" << buf3.data << std::endl;

    return 0;
}

위 예는 Buffer라는 템플릿 구조체의 템플릿 인자로 넘겨지는 N의 값에 따라 data 포인터에 다른 static 버퍼 배열의 주소를 할당하고 있다. 실행 결과는 아래와 같다.

buff_1024:00007FF70C7C24A0, buff_2048:00007FF70C7C34A0
Buff<1024>:00007FF70C7C24A0
Buff<1024>:00007FF70C7C24A0
Buff<2048>:00007FF70C7C34A0

템플릿 인자로 1024를 넘긴 buf1과 buf2에는 buff_1024의 주소가 할당되고 2048을 넘긴 buf3의 data 포인터는 buff_2048의 주소를 가리키고 있는것을 확인할 수 있다.

위 조건문은 실행시간에 변수를 보고 결정된 것이 아니라 컴파일 타임에 템플릿인자를 대상으로 조건식을 판별하고 결정된 것에 주목하자. 그리고 위 예는 설명을 위해 아주 간단하게(그리고 억지스럽게) 작성 되었지만 C++20 부터 추가되는 Concepts와 함께 사용하게 되면 보다 다양한 연산을 컴파일 타임에 수행할 수 있다.

마치며

이상으로 컴파일 상수로써의 constexpr, 컴파일 타임 실행 함수로써 constexpr, 컴파일 타임 조건문으로써 constexpr에 대해 살펴 보았다.

다른 것은 기억하지 못하더라도 constexpr은 '컴파일 타임에 모든 것이 결정 된다'라는 것 하나는 꼭 기억하도록 하고, 이어 소개 되는 [C++20] Concepts에서 템플릿 인자 이용한 템플릿 프로그래밍 기법에 대해서도 살펴보자.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!