ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Type deduction : decltype
    C++/Effective Modern C++ 2022. 1. 4. 14:12

    형식 연역 (Type deduction)은 소스코드에서 특정한 코드가 어떤 것으로 바뀌는 것 입니다.

    여기서 '특정한 코드'란 template, auto, decltype 등 이 있으며

    바뀌게 되는 '어떤 것' 에는 자료형부터, 함수, 람다식 등이 있습니다.

     

    이전 두 글에서, templateauto의 형식 연역 과정을 살펴보았습니다.

    두 키워드 모두 몇 가지의 특이사항만 제외한다면 동일한 규칙이 적용됨을 알 수 있었습니다.

    decltype은 이전 두 키워드와는 조금 다른 방식의 연역이 적용됩니다.

     

    이번 글에서는 decltype에 대하여 알아보도록 하겠습니다.

     


     

    decltype은 형식 지정자 키워드입니다.

    기본적인 사용은 아래와 같습니다.

    const int i = 0;
    
    bool f(const int& param);
    
    struct s{
        int x, y;
    };
    
    template<typename T>
    class cls{};
    cls<int> c;

    위와 같은 선언들이 있을 때, decltype이 연역하는 형식은 아래 표와 같습니다.

    decltype Type
    decltype(i) const int
    decltype(f) bool(const int&)
    decltype(s::x) int
    decltype(c) cls<int>

    주어진 이름 혹은 표현식의 구체적인 형식이 그대로 연역됨을 볼 수 있습니다.

     

    decltype는 함수의 반환 형식이 일정하지 않을 때에 주로 사용됩니다.

    예를 들어, 함수의 반환 형식이 템플릿 매개변수에 의존하는 경우가 있겠습니다.

    조금 더 구체적으로, 컨테이너의 색인 하나를 돌려주는 함수 ([]연산자와 유사한 기능)를 생각해보도록 하겠습니다.

     

    우선적으로 생각해보아야 할 것은 형식 T에 대한 operator[] 연산의 반환 형식입니다.

    operator[]연산은 대부분 T&를 반환하지만, 예외가 존재합니다.

    예를 들어, vector<bool>에 대한 operator[]연산은 bool&이 아닌 bool을 반환합니다.

    위와 같은 특이성을 고려하여, 색인 함수를 만들어 볼 경우 아래와 같이 됩니다.

    template<typename Container, typename Index>
    auto indexing(Container& c, Index i) -> decltype(c[i]) {
        return c[i];
    }

    위 함수의 반환형식 auto는 형식 연역과는 관계가 없고, 뒤의 후행 반환 형식 ( -> decltype(c[i]) ) 을 나타내기 위한 구문입니다.

    auto가 선언되는 지점에는 c와 i가 선언되지 않았으므로, 위와 같이 후행 반환 형식을 이용하여 반환 형식을 지정합니다.

    이제부터 위 함수를 다듬어 보도록 하겠습니다.

     

    1. C++14 : auto만 사용해보기

     

    더보기

    C++14에서부터, 모든 람다식과 함수의 반환 형식 연역을 허용합니다.

    따라서, 위 indexing 함수는 후행 반환 형식을 생략해도 컴파일이 됩니다.

    template<typename Container, typename Index>
    auto indexing(Container& c, Index i) {
        return c[i];
    }

    하지만, 위와 같이 후행 반환 형식을 생략할 경우 컴파일러는 auto에 템플릿 형식 연역 규칙을 적용합니다.

    이것이 소스코드가 정상적으로 작동한다고 서술하지 않고, 컴파일이 된다고 서술한 이유입니다.

     

    아래의 코드를 살펴보도록 하겠습니다.

    std::vector<int> v(10);
    indexing(v, 5) = 10;

    벡터의 5번째 원소에 접근해서 그 값을 10으로 변경하는 코드입니다.

    위와 같은 코드에서 후행 반환 형식을 생략한 indexing 함수를 사용할 경우 컴파일이 되지 않습니다.

    템플릿 형식 연역을 적용할 경우 표현식의 참조성이 무시되기 때문에, indexing(v, 5)의 결과는 int&가 아닌 int즉, r-value가 되기 때문입니다.

     

     

    2. decltype(auto)

     

    더보기

    후행 반환 형식을 생략하고, 기존의 결과를 그대로 가져오는 방법은 decltype(auto)입니다.

    decltype(auto)를 적용한 소스 코드는 다음과 같습니다.

    template<typename Container, typename Index>
    decltype(auto) indexing(Container& c, Index i) {
        return c[i];
    }

    이 경우 indexing 함수는 operator[]과 동일하게 작동합니다. 

     

    decltype(auto)는 함수 반환 형식 외에 변수 초기화에도 사용이 가능합니다.

    int i;
    const int& a = i;
    auto b = a;
    decltype(auto) c = a;

    위 소스코드에서 b와 c에 연역되는 형식은 아래 표와 같습니다.

    Expression Type
    auto b = a; int
    decltype(auto) c = a; const int&

    auto의 형식 연역 과정에서 참조성과 상수성이 무시되는 것과 달리, decltype(auto)를 적용하면 그러한 수식들을 유지할 수 있음을 볼 수 있습니다.

     

     

    3. r-value에 대한 Case

     

    더보기

    decltype(auto)를 적용한 indexing 함수는 수정 가능한 Container를 l-value참조로써 받습니다.

    여기에는 처리하기 곤란한 특이 케이스가 존재합니다.

    예제를 살펴보도록 하겠습니다.

    std::vector<int> int_vec_factory() {
        return std::vector<int>(10);
    }
    
    auto s = indexing(int_vec_factory(), 5);

    indexing 함수는 2번째 문단에서 수정한 것과 같습니다.

    위 코드는 팩토리 함수가 반환한 벡터의 5번째 원소를 복사하는 코드입니다.

    기존의 indexing 함수의 경우, r-value를 인자로 받을 수 없기 때문에 컴파일 되지 않습니다.

    이것을 해결하기 위해 r-value함수와 l-value함수를 두개 사용할 수 있지만, 이것은 관리해야 할 함수가 두 개가 된다는 문제점이 있습니다.

     

    이것은 보편 참조(Universal reference)로 해결할 수 있습니다.

    수정된 indexing 함수는 아래와 같습니다.

    template<typename Container, typename Index>
    decltype(auto) indexing(Container&& c, Index i) {
        return c[i];
    }

    이렇게 수정할 경우 r-value와 l-value모두 참조할 수 있게 됩니다.

    STL에 쓰이는 방식과 동일하게 한가지만 더 수정하면 아래와 같습니다.

    template<typename Container, typename Index>
    decltype(auto) indexing(Container&& c, Index i) {
        return std::forward<Container>(c)[i];
    }

    std::foraward<>는 인자값이 r-value인지 l-value인지 판단하여 캐스팅 해 주는 함수입니다.

    만약 컴파일러가 C++14이후의 버전이 아니라면, 아래와 같은 방식으로 사용할 수 있습니다.

    template<typename Container, typename Index>
    auto indexing(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i) {
        return std::forward<Container>(c)[i];
    }

     

     

    4. 기타 : decltype의 예외사항

     

    더보기

    decltype을 이름에 사용할 경우 그 형식이 됩니다.

    그런데 여기서 이름보다 복잡한 l-value 표현식에 대해서는 decltype은 l-value 참조를 보고합니다.

    즉, 형식 T에 대한 특정 이름이 아닌 l-value 표현식에 대하여 decltype은 T&를 보고합니다.

    대부분의 l-value 표현식에서는 l-value 참조가 포함되어 있기 때문에 문제가 되는 경우는 드물지만, 의외의 차이가 생기는 부분이 존재합니다.

    예제를 살펴보도록 하겠습니다.

    int x = 0;

    위 x에 대하여 아래 표의 decltype을 보도록 하겠습니다.

    Expression Type
    decltype(x) int
    decltype((x)) int&

    "(x)"는 이름보다 복잡한 표현식의 간단한 예 입니다.

    이 경우, int&이 되는 것을 볼 수 있습니다.

     

    조금 더 위험한 예제를 살펴보도록 하겠습니다.

    decltype(auto) function1() {
        int x = 0;
        return x;
    }
    
    decltype(auto) function2() {
        int x = 0;
        return (x);
    }

    위 예제의 function2 또한 int&를 반환합니다.

    그런데 여기서 반환하는 int&가 지역변수 라는 것이 중요합니다.

    가독성과, 표현식의 확실한 표현을 위해 괄호를 자주 이용하는 것은 좋은 습관이지만

    위와 같이 형식이 변하는 경우도 존재하므로, 위 경우를 확실히 숙지하는 것이 좋겠습니다.

     


     

    decltype은 대부분의 경우에서 그 이름에 걸맞게 declared type을 반환합니다.

    하지만, 위의 예제에서 살펴본 것 처럼 예상하지 않은 결과가 나오는 경우도 있습니다.

    그 경우가 세분화 되어있지는 않으니 반드시 숙지하는 것이 좋겠습니다.

    읽어주셔서 감사합니다.

    댓글

Designed by Tistory.