티스토리 뷰

decltype은 주어진 이름이나 표현식의 형식(type)을 알려주는 키워드입니다. 대부분의 경우에서 decltype이 사용자가 예측한 그 형식을 말해 주지만, 아주 가끔 예상 밖의 결과를 제공하기도 합니다.

 

먼저, 대부분의 경우부터 살펴보겠습니다. 템플릿과 auto의 형식 연역(항목 1항목 2 참고)에서 일어나는 일과는 달리. decltype은 주어진 이름이나 표현식의 구체적인 형식을 그대로 말해줍니다.

const int i = 0;                            // decltype(i)는 const int
bool f(const Widget& w)                // decltype(w)는 const Widget&
                                                    // decltype(f)는 bool(const Widget&)

struct Point{
    int x, int y;                                // decltype(Point::x) 및 decltype(Point::y)는 int
};

Widget w;                                    // decltype(w)는 Widget

if( f(w) ) ...                                    // decltype(f(w))는 bool

template<typename T>            // std::vector의 단순화 버전입니다.
class vector
{
public:
    ...
    T& operator[](std::size_t index);
};

vector<int> v;                                // decltype(v)는 vector<int>

if (v[0] == 0) ...                                // decltype(v[0])은 int&

 

C++11에서 decltype은 함수의 반환 형식이 그 매개변수 형식들에 의존하는 함수 템플릿을 선언할 때 주로 사용됩니다. 예를 들어, 컨테이너 하나와 색인 하나를 받고, 우선 사용자를 인증한 후 대괄호 색인화를 통해서(즉, 컨테이너[색인] 구문) 컨테이너의 한 요소를 돌려주는 함수를 작성한다고 가정하겠습니다. 이러한 함수의 반환 형식은 반드시 색인화 연산의 반환 형식과 동일해야 합니다.

decltype을 이용하면 상기 언급한 함수의 반환 형식을 다음과 같이 손쉽게 표현할 수 있습니다 :

template<typename Container. typename Index>                    // 작동하지만 좀 더 정제할 필요가 있습니다.
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
    authenticateUser();
    return c[i];
}

 

함수 앞에 auto를 지정하는 것은 형식 연역과는 아무런 관려이 없습니다. 여기서 auto는 C++11의 후행 반환 형식(trailing return type) 구문이 쓰인다는 점을 나타내는 것일 뿐입니다(반환 형식이 -> 다음 위치에 나타나는 것), 이러한 반환 형식 구문에는 반환 형식을 매개변수들을 이용해서 지정할 수 있다는 장점이 있습니다. 통상적인 방식으로 함수 이름 앞에서 반환 형식을 지정한다면, c와 i가 아직 선언되지 않았으므로 사용할 수 없습니다.

C++11에서는 람다 함수가 한 문장으로 이뤄져 있다면 그 반환 형식의 연역을 허용하며, C++14는 허용 범위를 확장하여 모든 람다와 모든 함수의 반환 형식 연역을 허용합니다. 따라서 C++14에서는 다음과 같이 사용할 수 있습니다 :

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];                    // 반환 형식은 c[i]로부터 연역됩니다.
}

 

항목 2에서 설명했듯이. 함수 반환 형식에 auto가 설정되어 있으면 컴파일러는 템플릿 형식 연역을 적용합니다. 지금 예에서는 이것이 문제가 됩니다. T 객체들을 담은 컨테이너에 대한 operator[] 연산은 대부분의 경우 T&를 돌려줍니다. 문제는, 항목 1에서 설명했듯이 템플릿 형식 연역 과정에서 초기화 표현식의 참조성이 무시된다는 점입니다.

std:deque<int> d;
...
authAndAccess(d,5) = 10;    // 사용자를 인증하고, d[5]를 돌려주고, 그 다음 10을 d[5]에 배정합니다.
                                            // 이 코드는 컴파일 되지 않습니다.

 

여기서 d[5]는 int&를 돌려주지만, auto 형식 연역 과정에서 경우 1에 의해 참조성이 제거 되어 반환 형식이 int가 됩니다. 함수의 반환값으로써 이 int는 하나의 오른값 이며, 결과적으로 위의 예제 코드는 오른값에 10을 배정하려고 하므로 컴파일이 되지 않습니다.

결국, authAndAccess를 사용자가 원하는 대로 작동하게 하려면, 함수의 반환 형식이 decltype 형식 연역이 적용되게 만들어야 합니다. 좀 더 구체적으로, authAndAccess가 c[i]의 반환 형식과 정확히 동일한 형식을 반환하도록 만들어야 합니다.

그것이 가능하도록, C++14에서는 decltype(auto) 지정자를 통해서 그러한 일이 가능하도록 만들었습니다. decltype과 auto가 동시에 나오는 것이 언뜻 모순처럼 보이지만, auto는 해당 형식이 연역되어야함 을 뜻하고, decltype은 그 연역 과정에서 decltype 형식 연역 규칙들이 적용되어야 함 을 뜻한다는 점에서 합당합니다.

template<typename Container, typename Index>        // 작동하지만 좀 더 정제할 필요가 있습니다.
decltype(auto) AuthAndAccess(Container c, Index i)
{
    authenticateUser();
    return c[i];
}

 

이제 authAndAccess의 반환 형식은 실제로 c[i]의 형식과 일치합니다. decltype(auto)를 함수 반환 형식에서만 사용할 수 있는 것은 아닙니다. 변수를 선언할 떄에도, 초기화 표현식에 decltype 형식 연역 규칙들을 적용하고 싶은 경우 이 지정자가 유용합니다 :

Widget w;

const Widget& cw = w;

auto myWidget1 = cw;                    // auto 형식 연역 : myWidget1의 형식은 Widget

decltype(auto) myWidget2 = cw;        // decltype 형식 연역 : myWidget2의 형식은 const Widget&

 

 

 

개운치 않은 사항


아직 개운치 않은 사항이 두 개 있습니다. 하나는 앞의 코드 블록에서 언급하고 설명하지 않은, authAndAccess의 정제 방법입니다. 먼저 이 부분을 짚어보겠습니다. authAndAccess의 함수의 C++14버전을 살펴보았을 때, 컨테이너 c는 비 const 객체에 대한 왼 값 참조로서 함수에 전달됩니다. 이는 함수가 돌려주는 컨테이너 요소를 클라이언트가 수정할 수 있도록 하기 위한 것입니다. 문제는, 이 때문에 함수에 오른값 컨테이너는 전달될 수 없다는 것입니다.

 

오른값 전달 불가

솔직히 오른값 컨테이너를 인자로 넘기는 것은 임시 객체로서의 오른값 컨테이너가 호출 문장의 끝에서 파괴되며, 컨테이너 안의 한 요소를 지칭하는 참조가 그 요소를 생성하는 문장의 끝에서 지칭 대상을 잃게 되므로 극단정인 경우(edge case)에 해당합니다. 하지만, authAndAccess에 임시 객체를 넘겨줄 수 있도록 만드는 것은 여전히 합당합니다. 다음 예처럼 클라이언트가 그냥 임시 컨테이너의 한 요소의 복사본을 만들고 싶을 수도 있기 때문입니다.

std::deque<std::string> makeStringDeque();            // 팩토리 함수

// makeStringDeque가 돌려준 deque의 다섯 번째 원소의 복사본을 생성합니다.
auto s = authAndAccess(makeStringDeque(), 5);

 

이런 용법을 지원하려면 authAndAcces가 왼값 뿐만이 아니라, 오른값도 받아들일 수 있도록 선언을 고쳐야 합니다. 오버로딩을 사용할 수도 있지만(왼값 매개변수 버전과 오른값 매개변수 버전), 그러면 관리해야할 함수가 두 개가 됩니다. 이를 피하는 한 가지 방법은 보편 참조 매개변수를 authAndAccess에 도입하는 것입니다 :

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);        // 이번에는 c가 보편 참조

 

컨테이너 매개변수가 값이 아닌 참조인 이유는 미지 형식의 불필요한 복사에 성능 하락이나, 객체가 잘리는 등의 문제를 피하기 위함입니다. 이제 변화된 선언에 맞게 템플릿의 구현도 고쳐질 필요가 있습니다. 구체적으로, 항목 25의 조언에 따라 다음과 같이 보편 참조에 std::forward를 적용합니다 :

template<typename Container, typename Index>            // 최종 C++14 버전
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

 

이제 사용자가 원하는 방식으로 작동하는 템플릿이 만들어졌지만 이는 C++14 컴파일러에서 가능한 문법이며, C++11버전의 컴파일러에서는 다음과 같이 사용할 수 있습니다 :

template<typename Container, typename Index>            // 최종 C++14버전
auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

 

decltype의 예상 밖 결과

개운치 않은 사항 두 번째는, 도입부에서 설명했듯이 decltype이 아주 가끔 예상 밖의 결과를 제공한다는 것입니다. decltype을 이름에 적용하면, 그 이름에 대해 선언도니 형식이 산출됩니다. 대체로 이름은 왼값 표현식이나, 이 점이 decltype 행동에 영향을 주지는 않습니다. 하지만, 이름보다 복잡한 왼값 표현식(expression) 에 대해서는 일반적으로 decltype이 항상 왼쪽값 "참조" 를 보고합니다. 즉, 이름이 아니면서 형식이 T인 왼값 표현식에 대해 decltype은 T&를 보고합니다.

decltype(auto) f1()
{
    int x = 0;
    retrun x;                // decltype(x)는 int이므로 f1은 int를 반환합니다
}

decltype(auto) f2()
{
    int x = 0;
    return (x);                // decltype((x))는 int&이므로 f2는 int&를 반환합니다
}

 

이 예의 교훈은, decltype(auto)는 아주 조심해서 사용해야 한다는 것입니다. 겉보기에 중요하지 않은 세부사항이라도 decltype(Auto)가 보고하는 형식에 영향을 미칠 수 있습니다. 애초에 예상했던 형식이 실제로 연역되었는지 확인하고 싶다면 항목 4에서 설명하는 기법들을 사용하면 됩니다. decltype이 가끔 뜻밖의 형식을 연역하기도 하지만, 이것은 정상적인 상황이 아니며, 보통 decltype은 기대한 바로 그 형식을 산출합니다. 이 경우 decltype은 그 몇칭에 걸맞게 행동합니다. 즉, 주어진 이름의 선언된 형식(declared type)을 돌려줍니다.

 

 

 

기억해 둘 사항들


  • decltype은 항상 변수나 표현식의 형식을 아무 수정없이 보고합니다.
  • decltype은 형식이 T이고, 이름이 아닌 왼값 표현식에 대해서는 항상 T& 형식을 보고합니다.
  • C++14는 decltype(auto)를 지원합니다. decltype(auto)는 auto처럼 초기치로부터 형식을 연역하지만, 그 형식 연역 과정에서 decltype의 규칙들을 적용합니다.

 

 

 

레퍼런스


[Effective Modern C++ - C++11과 C++14를 효과적으로 사용하는 42가지 방법] 항목 3 : decltype의 작동 방식을 숙지하라

 

댓글