본문 바로가기

진리는어디에/C++

[C++] constexpr 함수

들어가며

이전 포스트 'constexpr 상수'에서는 컴파일 타임 상수로써의 constexpr 키워드에 대해 살펴 보았다.이번 포스트에서는 컴파일 타임 함수로써의 constexpr 함수의 개념과 특징에 대해 살펴 보는 시간을 갖도록 하겠다.

constexpr 함수의 개념

먼저 아래의 코드 샘플을 살펴 보자. constexpr 함수의 형식은 기존 C 또는 C++에서의 함수와 매우 비슷하다. 다만 앞에 constexpr 키워드를 붙여 주기만 하면 된다.

// C/C++의 일반적인 함수
//
// int add(int a, int b)
// {
//     return a + b;
// }

// constexpr 키워드가 붙은 함수
constexpr int add(int a, int b)
{
    return a + b;
}

constexpr 이라는 키워드는 '컴파일 타임에 결정' 된다는 의미를 가지고 있다. 이전 포스트에서 봤던 constexpr 상수는 컴파일 타임에만 값이 결정 될 수 있는 변수를 의미하고, 지금 소개 되는 constexpr 함수는 컴파일 타임에 함수의 연산을 실행하겠다는 의미다.

constexpr 함수 : 컴파일 타임에 연산을 실행하는 함수

 

아래의 constexpr 함수를 호출하는 예제를 살펴 보자.

constexpr int add(int a, int b)
{
    return a + b;
}

int main()
{
    int x = 1;
    int y = 1;
    
    int n1 = add(1, 1); // 함수의 인자로 리터럴 정수를 사용
    int n2 = add(x, y); // 함수의 인자로 변수를 사용
    
    return 0;
}

앞에서 constexpr 함수는 함수의 인자로 사용 되는 값들을 컴파일 타임에 알 수 있다면 함수의 연산을 컴파일 타임에 실행 한다고 했다. 위 예제의 11 라인과 12 라인의 add 함수 호출을 살펴 보면서 컴파일 타임에 함수를 실행한다는 것이 무엇인지 알아 보도록 하자.

11 라인의 'add(1, 1)' 의 경우에는 리터럴 상수 1을 이용하여 호출하고 있다. 리터럴 상수의 경우 컴파일 타임에 그 값을 알 수 있으므로 위 add 함수 호출은 컴파일 타임에 계산 되어 2로 변경 된다.

반면에 12라인의 add(x, y)는 변수를 인자로 받고 있다. 변수는 런타임에 사용자 입력 등으로 언제든 값이 변경 될 수 있으므로 constexpr 한정자를 붙인 함수라고 하더라도 컴파일 타임에 실행하지 않고 런 타임에 실행하게 된다.

  • 함수의 인자 값을 컴파일 타임에 결정할 수 있으면 "컴파일 타임에 함수를 실행"
  • 함수의 인자 값을 컴파일 타임에 결정할 수 없으면 "런 타임에 함수를 실행"

간.단.한 함수를 만들 때 constexpr을 추가하여 컴파일 타임에 연산할 수 있다면 런 타임에 실행될 연산을 줄일 수 있으므로 어플리케이션의 성능을 향상할 수 있고 혹시 컴파일 타임에 실행하지 못했다고 하더라도 런타임에 일반 함수 처럼 실행되므로 손해 볼 것 없는 장사다('간단한' 이라는 단어를 강조 했음에 유의하자).

템플릿 인자로써 constexpr 함수

C++은 함수나 구조체, 클래스를 선언할 때 템플릿을 사용하여 템플릿 인자로 넘어겨지는 타입에 대한 동일한 코드를 반복적 코드를 생성할 수 있다. 템플릿은 인자로 타입 뿐 아니라 정수형 상수를 받을 수도 있는데 이렇게 인자로 넘겨지는 정수형 상수는 반드시 컴파일 타임에 값을 알 수 있어야 한다.

앞에서 살펴본 constexpr 함수가 리터럴 인자를 받는 다면 컴파일 타임에 실행 될 수 있고, 컴파일 타임에 실행되어 값을 결정 된다는 것은 템플릿 인자로도 사용될 수 있다는 뜻이다.

// 템플릿 인자로 정수를 받는 구조체
template <class T, int N>
struct Buffer
{
    T data[N];
};

constexpr int add(int a, int b)
{
    return a + b;
}

int main()
{
    int n = 1024;
    
    Buffer<int, 1024> buf1;  // 정수형 리터럴 상수를 사용 했으므로 ok
    Buffer<int, n> buf2;     // 변수이므로 컴파일 타임에 결정할 수 없다. error!
    Buffer<int, add(1, 2)> buf3; // 컴파일 타임에 실행 되는 constexpr 함수. ok
    Buffer<int, add(n, n)> buf4; // 런 타임에 실행 되므로 error
    
    return 0;
}

위의 템플릿 구조체를 다양한 방법으로 선언하고 있는 예제를 살펴 보자.

  • 17라인 : 정수 타입 리터럴 상수이므로 컴파일 타임에 값을 알 수 있다.
  • 18라인 : 변수는 템플릿 인자로 사용할 수 없다.
  • 19라인 : 템플릿 인자로 함수를 호출하지만 리터럴 상수를 인자로 받는 constexpr 함수이기 때문에 컴파일 타임에 값을 알 수 있다.
  • 20라인 : 같은 constexpr 함수를 호출하지만 인자로 변수를 넘기고 있으므로 컴파일 타임에 실행 되지 않는다. 그러므로 템플릿 인자로 사용할 수 없다.

constexpr 함수의 제약

앞에서 constexpr 함수의 개념에 대해 설명할 때 '간단한' 함수를 작성할 때 사용하면 좋다고 했다. 함수는 컴파일 타임에 연산을 미리해 둘수도 있고, 템플릿 인자로도 사용될 수 있는 강력함을 가지고 있지만 반면에 만만치 않은 제약사항도 있다.

constexpr 함수의 경우 C++11 버전 까지는 함수 내에 단 하나의 return 문만 가질 수 있었다. 다행히 C++14 부터 이런 제약은 없어졌지만 함수 내에서 런 타임 동안 값이 변경 될 수 있는 연산을 수행 할 수 없다는 제약은 여전하다. 예를 들어 constexpr 함수 내부에서는 파일을 오픈하거나 동적으로 메모리를 할당 할 수 없다.

constexpr 함수의 제약 사항 :

  • 함수 내에서 파일을 오픈하거나 동적으로 메모리를 할당 할 수 없다.
  • constexpr 함수는 가상 함수가 될 수 없다.

위의 제약 사항 외에도 다양한 제약 사항이 있지만 일단 이 정도만 알아둬도 실제 코드를 작성함에 있어서 큰 어려움은 없을 것이다. 혹시나 제약 사항을 위반하더라도 컴파일 타임에 오류를 발생 시키므로 컴파일러를 통해 오류 내용을 충분히 파악 할 수 있을 것이다.

보다 자세한 C++ 버전별 제약 사항은 아래 사이트에서 설명하고 있으므로 관심이 있다면 살펴 보도록 하자.

 

constexpr specifier (since C++11) - cppreference.com

[edit] Explanation The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile time constant expressions are allowed (provided that

en.cppreference.com

마치며

constexpr 함수를 요약하자면 constexpr 이라는 키워드는 컴파일 타임에 결정 된다라는 의미이고 constexpr 함수라는 것은 컴파일 타임에 실행될 수 있는 함수다. 제약 사항이 있어 복잡한 함수 보다는 간단한 함수를 만들 때 사용하면 좋고, 단순 함수 뿐만 아니라 템플릿 인자로도 활용될 수 있는 강력함을 가지고 있다.

다음 포스트 [C++] constexpr 조건문에서는 constexpr을 이용해 컴파일 타임에 조건을 조사할 수 있는 방법과 활용 방법에 대해 살펴 보도록 하겠다.

부록 1. 같이 읽으면 좋은 글

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