본문 바로가기
C++/Effective Modern C++

decltype의 작동 방식

by COCO1337 2020. 6. 26.

decltype은 주어진 이름이나 표현식의 형식을 알려준다.

대부분의 경우 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, y;     		// decltype(Point::x) 는 int, 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은 함수의 반환 형식이 그 매개변수 형식들에 의존하는 함수 템플릿을 선언할 때 주로 쓰인다.

 

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

함수 이름 앞의 auto는 C++11의 후행 반환 형식 구문이 쓰인다는 점을 나타낸다. 즉 함수의 반환 형식을 이 위치가 아니라 매개변수 목록 다음에 선언하겠다는 것을 나타내는 것 뿐이다.

 

C++11은 람다 함수가 한 문장으로 이루어져 있다면 그 반환 형식의 연역을 허용하며, C++14는 허용 범위를 더 확장해서 모든 람다와 모든 함수의 반환 형식 연역을 허용한다.

template<typename Container, typename Index>		// C++14, 아주 정확하진 않음
auto authAndAccess(Container& c, Index i)
{
	authenticateUser();
	return c[i];		// 반환 형식은 c[i]로부터 연역된다.
}

함수 반환 형식에 auto가 지정되어 있으면 컴파일러는 템플릿 형식 연역을 적용한다.

문제는, 템플릿 형식 연역 과정에서 초기화 표현식의 참조성이 무시된다는 점이다.

std::deque<int> d;
authAndAccess(d, 5) = 10;		// 컴파일 되지 않는다.

d[5]가 int&를 돌려주지만 auto 반환 형식 연역 과정에서 참조성이 무시되어 최종 반환 형식은 int가 된다. 반환값으로서의 int는 오른값이며, 오른값 int에 10을 배정하는 것은 금지되어 있기 때문에 컴파일 되지 않는다.

 

template<typename Container, typename Index>		// C++14
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&

 

다시 C++14 버전의 선언으로 돌아와서,

컨테이너 c는 비const 객체에 대한 왼값 참조로서 함수에 전달된다. 하지만 왼값뿐만 아니라 오른값도 받아들이도록 하기 위해 보편 참조를 사용한다.

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

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

decltype에 이름을 적용하면 그 이름에 대해 선언된 형식이 산출된다. 대체로 이름은 왼값 표현식이다. 하지만 이름보다 복잡한 왼값 표현식에 대해서는 일반적으로 decltype은 왼값 참조를 보고한다. 즉 이름이 아닌, 형식이 T인 왼값 표현식에 대해 decltype은 T&를 보고한다.

int x = 0;		// x는 변수의 이름이므로 decltype(x)는 int
int(x) = 0;		// x는 하나의 왼값, (x)라는 표현식도 왼값이다. 즉 decltype((x))는 int&

decltype(auto)는 보고하는 형식에 영향을 미칠 수 있기 때문에 사용에 주의해야 한다.


Conclusion

- decltype은 항상 변수나 표현식의 형식을 아무 수정없이 보고한다.

- decltype은 형식이 T이고 이름이 아닌 왼값 표현식에 대해서는 항상 T&형식을 보고한다.

- C++14는 decltype(auto)를 지원한다. auto처럼 초기치로부터 형식을 연역하지만 그 과정에서 decltype 규칙을 적용한다.


Reference

Effective Modern C++ 항목 3: decltype의 작동방식을 숙지하라

반응형

댓글