Effective Modern C++ #3 decltype 작동 방식을 숙지하라.
3. decltype 작동 방식을 숙지하라.
decltype (declared type) 은 객체의 주어진 이름, 표현식 형태 등을 알려준다.
const int i = 5; // decltype(i) = const int
bool f(const Widget & w) // decltype(f) = bool(const Widget &), decltype(w) = const Widget &
하지만 예상하지 못한 동작을 하는 케이스가 존재한다.
예를 들어, vector, deque 등 컨테이너의 operator[] 반환 형식은 대체로 T& 이다.
하지만 std::vector<bool> 의 operator[] 는 bool&이 아니다.
따라서 decltype을 사용하여, 이런 반환 형식을 일반화하는 예시를 확인해보자.
예시)
#include <iostream>
#include <vector>
template<typename Container, typename Index>
auto ret(Container & c, Index i) -> decltype(c[i]) {
return c[i];
}
int main() {
std::vector<int> v(5);
ret(v, 2) = 5;
std::cout << v[2] << std::endl;
}
함수의 반환 타입은 auto 이나, C++11 에서는 trailing return type 구문 (후행 반환 형식 / -> 뒤에 반환 형식 표현) 을 사용하지 않으면 컴파일 에러가 발생한다.
trailing return type 구문은 기존 return 구문과 다르게, 함수 인자로 들어오는 값을 사용하여 반환 형식을 지정할 수 있는 장점이 있다.
C++ 14 이상부터는 `-> decltype(c[i])` 와 같은 trailing return type 없이 `auto` 와 `return c[i];` 만으로 컴파일러가 반환 형식을 예측하게 할 수 있다.
따라서 auto 는 컴파일러가 '반환 형식을 알아서 예측' 하라는 역할만 하게 된다.
여기서 예상하지 못한 동작이 발생한다.
일단 위 코드에서 `-> decltype(c[i])` 를 제거한다.
컨테이너가 함수 인자로 전달되며, template 연역 과정에서 참조성은 사라지고 r-value 로 반환된다. (1. 템플릿 형식 연역 규칙을 숙지해라 참고)
따라서 `ret(v, 2) = 5;` 는 r-value 를 r-value 에 할당하게 되므로 컴파일 에러가 발생한다.
해결 방법은 `auto` 대신 `decltype(auto)` 를 사용하는 것이다.
- auto : 컴파일러가 '반환 형식을 알아서 예측' 하라
- decltype : 반환 형식을 추론할 때에는 decltype 규칙을 적용해라
이를 사용하면, l-value인 `v[2]` 를 그대로 반환할 수 있다.
함수에 l-value 가 아닌 r-value 를 인자로 넘기고 싶은 경우, `std::forward` 를 활용하고, 인자를 보편 참조 형태 (&&)로 변경하면 된다.
이렇게 코드를 수정하는 경우 l-value, r-value 에 모두 대응 가능하다.
// C++14 이하
template<typename Container, typename Index>
auto ret(Container && c, Index i) -> decltype(std::forward<Container>(c)[i]) {
return std::forward<Container>(c)[i];
// C++14 이상
template<typename Container, typename Index>
decltype(auto) ret(Container && c, Index i) {
return std::forward<Container>(c)[i];
하지만 decltype(auto) 마저도 예상하지 못한 동작을 하는 케이스가 있다.
예를 들어, l-value 를 () 괄호로 감싸는 경우 decltype 이 추론하는 형식이 달라진다.
int x = 0;
decltype(x); // int
decltype((x)); // int &
아래 예시는 () 와 같은 작성 습관에 따라 decltype(auto) 의 반환 추론이 달라져, 내부 변수 참조를 전달하는 문제 있는 코드이다.
#include <iostream>
#include <vector>
decltype(auto) ret() {
int a = 5;
return (a); // & int
}
int main() {
std::cout << ret() << std::endl;
}