일반적인 설명

 constexpr 키워드는 c++11에서 소개되고 c++14에서 개선되었습니다. (visual studio 2015부터 지원합니다.) 

 constexpr은 상수식 이며 const처럼 변수에 적용 할 수 있으며 해당 변수에 대한 변경을 시도하면 

 컴파일러는 에러를 발생합니다. 

 const와는 다르게 함수와 생성자에 적용 할 수 있습니다. constexpr로 지정된 값이나 리턴 값이 상수이며

 가능한 경우 컴파일 타임에 계산됩니다. 

 템플릿 인수 및 배열 선언 같은 const 정수 값이 사용되는 곳에 constexpr 정수 값이 사용 될 수 있습니다.

 constexpr이 컴파일 타임에 값이 계산되면 프로그램 실행 속도가 빨라지며 메모리 사용량이 줄어 듭니다. 

 컴파일 타임의 상수 연산에 대한 복잡성과 잠재적인 문제를 예방하기 위해서 c++ 14에서는 상수 표현식은

 리터럴 유형이어야 합니다. 

constexpr 리턴 값

 constexpr 함수의 리턴 값이 constexpr 속성을 가지려면 리터럴 타입이어야 합니다.

const int64_t const_mypow(int64_t itarget, int isquare)
{
    if (isquare <= 1)  return itarget;
    return itarget * const_mypow(itarget, isquare - 1);
}

// constexpr의 리턴 값을 가지는 리터럴 함수
constexpr int64_t constexpr_mypow(int64_t itarget, int isquare)
{
    if (isquare <= 1)  return itarget;
    return itarget * constexpr_mypow(itarget, isquare - 1);
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::array<int, 256>                        arr;            // 가능 = 상수
    std::array<int, const_mypow(2, 16)>         const_arr;      // error C2975 컴파일 상수식이 필요합니다.
    std::array<int, constexpr_mypow(2, 16)>     constexpr_arr;  // 가능 = constexpr 리턴 함수
    return 0;
}

 위의 예제 코드를 보시면 array는 컴파일 타임에 배열 크기가 필요하기 때문에 const를 리턴하는 함수를 사용해도

 선언을 할 수 없습니다. 하지만 constexpr을 리턴하는 함수를 통해서는 array의 배열 크기를 지정할 수 있습니다. 

리터럴 타입

 리터럴 타입은 컴파일 타임에 정해 질 수 있는 타입입니다. c++ 기본 제공 타입은 리터럴 타입입니다. 

constexpr 변수

 const와 constexpr 변수의 가장 큰 다른점은 const는 런타임까지 초기화를 지연 할 수 있지만

 constexpr는 컴파일 타임에 초기화 되어야 합니다. 모든 constexpr변수는 const 속성을 가집니다. 

  • 변수가 리터럴 타입이면 초기화 될 때 constexpr로 선언 할 수 있습니다. 
  • constexpr 선언된 생성자를 사용해서 초기화하면 constexpr로 선언 할 수 있습니다. 
  • 참조가 constexpr로 선언되려면 두가지 조건이 충족되어야 합니다.
    • 참조 객체가 상수 식으로 초기화 되어야 합니다.
    • 초기화 되는 동안 발생하는 어떠한 암시적 변환도 상수 식이어야 합니다.
class Rect
{
public:
    constexpr Rect(int width, int height) :m_iwidth(width), m_iheight(height)
    {}

    constexpr int getArea() const { return m_iwidth * m_iheight;}
private:
    int m_iwidth;
    int m_iheight;
};  
 
// Rect라는 객체를 constexpr로 호출 할 수 있습니다.
// constexpr 생성자가 존재하기 때문에 가능합니다. 
constexpr Rect ce_rect = Rect(10, 10);
ce_rect.getArea();                          // const로 선언된 함수를 호출 할 수 있습니다.

constexpr int x = 205
constexpr float y{ 15 };
constexpr int i;                            // Error 초기화되지 않음
int j = 0;
constexpr int k = j + 1;                    // Error j는 상수표현식이 아닙니다.

constexpr 함수

 constexpr 함수에서 주의 할 점은 constexpr 함수로 선언되어 있다고 해서 무조건 컴파일 타임에 

 리턴값이 산출되지 않는다는 것입니다. constexpr 함수는 입력되는 인자값에 따라서 다르게 동작합니다.

 입력되는 인자 값이 컴파일 시점 상수라면 constexpr함수로 동작합니다.

 입력되는 인자 값이 컴파일 시점 상수가 아니라면 일반 함수 처럼 동작합니다.

 이렇게 동작하기 때문에 하나의 함수 정의로 두가지 기능을 제공하는 장점이 있습니다. 

constexpr 함수 동작 방식

// mypow는 constexpr 함수입니다. 
// 전달 받은 인자가 runtime에 초기화된다면 일반 함수 처럼 동작
// 전달 받은 인자가 컴파일 타임에 초기화된다면 일반 함수 처럼 동작
constexpr int64_t mypow(int64_t itarget, int isquare)
{
    if (isquare <= 1)  return itarget;
    return itarget * mypow(itarget, isquare - 1);
}

int _tmain(int argc, _TCHAR* argv[])
{
    // 런타임에 초기화되는 randow 변수
    auto current = std::chrono::system_clock::now();
    auto duration = current.time_since_epoch();
    std::mt19937_64 getMT(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
    std::uniform_int_distribution<int64_t> uniformDist(1, 10);

    // constexpr 함수 mypow를 런타임에 초기화되는 인자로 호출 
    // 일반 함수처럼 동작 합니다. 
    auto value1 = mypow(uniformDist(getMT), static_cast<int>(uniformDist(getMT)));
    
    // constexpr 함수 mypow를 constexpr 인자로 호출 constexpr 값을 리턴
    constexpr auto value2 = mypow(10, 2);

    std::array<int, value1> array1;
    std::array<int, value2> array2;

    return 0;
}

 예제를 보면 mypow라는 constexpr 함수를 확인 할 수 있습니다. 입력되는 값이 random으로 전달되는 

 mypow(uniformDist(getMT), uniformDist(getMT))형식으로 호출하면 리턴 값이 constexpr이 될 수 없고 일반 함수처럼

 동작합니다. 

 mypow(10, 2) 형식으로 호출하면 리턴 값이 constexpr로 설정되어 constexpr 값으로 리턴값을 받을 수 있으며

 컴파일 타임에 초기화 됩니다. 

컴파일러로 확인한 value3 값 컴파일전에 값이 설정되어 있습니다.

실제로 디버그를 통해서 디버깅해보면 mypow(10, 2) 내부로 들어가지 않고 바로 값이 셋팅되어 있는 것을

확인 할 수 있습니다.

constexpr c++14 개선 사항

// c++ 11에서는 한줄의 리턴식으로 표현 해야 하는 제한이 있습니다.
constexpr int64_t mypow(int64_t itarget, int isquare)
{
    return (isquare <= 1) ? itarget : itarget * mypow(itarget, isquare - 1);
}

// c++ 14에서는 한줄로 표현하는 제한이 사라졌습니다. 
constexpr int64_t mypow(int64_t itarget, int isquare)
{
    if (isquare <= 1)  return itarget;
    return itarget * mypow(itarget, isquare - 1);
}

// 루프를 통해서도 구현 가능
constexpr int64_t mypow(int64_t itarget, int isquare)
{
    for (int i = 1; i < isquare; i++)
    {
        itarget *= itarget;
    }
    return itarget;
}

c++ 11에서는 constexpr 함수는 하나의 리턴 식으로 표현해야 했는데 c++ 14에서는 일반적인 함수처럼 호출 할 수

있습니다.  

vs2017 부터는 constexpr 함수 내부에서 if와 switch, 루프(for, do-while, while)의 구문을 사용할 수 있습니다. 

constexpr함수 조건

 constexpr 함수를 구현하기 위해서는 아래의 내용을 만족 해야 합니다.

  • constexpr 함수는 리터럴 타입만 받아들이거나 리턴합니다. 
  • constexpr 함수는 재귀적일 수 있습니다. 
  • virtual 함수가 될 수 없습니다.
  • Body에서 가능한 구문
    • null Statement
    • static_assert
    • typedef, using
    • 함수가 생성자가 아니라면 하나의 리턴 구문 필요함
    • 등등...
  • Body에서 불가능한 구문
    • goto 
    • try-block
    • 초기화 수행이 없는 변수 정의
    • 리터럴타입이 아닌 변수 정의
    • Static 변수
    • tls 변수(thread local storage)
    • 등등...
// constexpr 함수에서 사용하지 못하는 구문을 정리합니다. 
constexpr int64_t NotConstexprFunc(int64_t itarget, int isquare)
{
    goto FAIL;        // goto문을 사용할 수 없습니다.

    try{}             // try 구문 사용 할 수 없습니다.
    catch(...){}

    thread_local int t_var; // thread_local 변수는 사용할 수 없습니다.
    static int s_var; // static 변수 사용 할 수 없습니다.
    CUser user;       // 리터럴 타입아닌 변수 정의 불가 
    double var;       // 초기화 되지 않은 변수 정의 불가
    
FAIL:                 // 레이블을 사용할 수 없습니다.
}

 위의 예제는 constexpr 함수에서 사용 할 수없는 구문들의 예를 보여 드립니다. 

constexpr 이점

 컴파일 타임에 알려지는 값들의 이점은 읽기 전용 메모리 배치됩니다. 읽기 전용 메모리는 변경이 되지 않는다는

 보장이 있기 때문에 더욱 효율적인 수행이 가능하도록 지원합니다. 

'c++ > modern c++' 카테고리의 다른 글

[c++] std::mutex  (0) 2020.03.28
[c++17] string_view  (0) 2020.03.21
[c++] auto와 decltype 키워드  (0) 2020.03.15
[c++] std::move와 std::forward  (4) 2020.03.07
[c++] Move semantics  (0) 2020.03.07

+ Recent posts