스토리텔링 개발자

[Effective Modern C++] 3. decltype 본문

개발/Effective Modern C++

[Effective Modern C++] 3. decltype

김디트 2025. 2. 7. 11:27
728x90

항목 3. decltype의 작동 방식을 숙지하라

 

 

 

decltype
  • 주어진 이름이나 표현식의 타입을 알려준다.
  • 하지만 가끔 예상 밖의 결과를 제공하기도 한다.
  • C++11에서는 함수의 리턴 타입을 매개변수 타입들로 추론해야 하는 함수 템플릿을 선언할 때 주로 사용한다.

 

 

 

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

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

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

 

 

 

리턴 타입 추론으로 사용되는 decltype
template<typename Container, typename Index>
auto autoAndAccess(Container& c, Index i) -> decltype(c[i]) // 사실 좀 더 정제 가능하다
{
    authenticateUser();
    return c[i];
}
  • 함수 이름 앞의 auto는 타입 추론과는 아무런 관련이 없다.
    • C++11의 후행 리턴 타입(trailing return type) 구문이라는 사실을 선언할 뿐이다.
    • 후행 리턴 타입은, 리턴 타입을 매개변수들을 이용해서 지정할 수 있다는 장점이 있다.
  • C++11은 람다 함수가 한 문장으로 이루어져 있다면 리턴 타입 추론을 허용한다.
  • C++14는 허용 범위를 더욱 확장해서 모든 람다와 모든 함수의 리턴 타입 추론을 허용한다.
    • 심지어 return 문이 여러 개인 함수조차도 허용한다.
    • 그러므로 위 코드가 만약 C++14라면 그냥 함수 이름 앞의 auto만 남겨두어도 된다.
template<typename Container, typename Index>
auto autoAndAccess(Container& c, Index i) // C++14. 아주 정확하진 않다.
{
    authenticateUser();
    return c[i];
}
  • 위 코드의 문제점
    • T 객체를 담은 컨테이너의 operator[]는 대부분 T&를 돌려주지만..
    • 템플릿 타입 추론 과정에서 초기화 표현식의 참조성(&)이 무시될 수 있다.(항목 1 참조)
std::deque<int> d;
...
authAndAccesss(d, 5) = 10; // 컴파일 에러!
// d[5]의 결과값인 int&를 리턴하는 걸 예상했지만,
// 템플릿에서 참조가 제거되기 때문에 리턴 타입은 int이다.
// rvalue에 rvalue를 넣는 건 금지되어 있기 때문에 컴파일 에러가 발생한다.
  • 템플릿 타입 추론이 아니라, decltype 타입 추론 규칙이 적용되게 만들어야 한다.
  • decltype(auto) 를 사용한다.

 

 

 

decltype(auto)
  • auto는 해당 타입이 추론되어야 함을 뜻한다.
  • decltype은 그 추론 과정에서 decltype 타입 추론 규칙이 적용되어야 함을 뜻한다.
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) // C++14. 작동하지만 좀 더 정제 가능
{
    authenticateUser();
    return c[i];
}
  • 함수 리턴 타입 뿐 아니라 변수 선언 시에도, 초기화 표현식에도 적용할 수 있다.
Widget w;
const Widget& cw = w;

auto myWidget1 = cw; // auto 타입 추론이 적용되어, 타입은 Widget

decltype(auto) myWidget2 = cw; // decltype 타입 추론이 적용되어, 타입은 const Widget&

 

 

 

authAndAccess 추가로 정제해보기
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
  • 컨테이너 c는 비const 객체에 대한 lvalue로 함수에 전달된다.
    • 허나, 이 때문에 함수에 오른값 컨테이너는 전달할 수 없다는 문제가 있다.
  • rvalue 컨테이너를 넘기는 건 솔직히 극단적인 상황(edge case)이긴 하다.
    • 하지만, 임시 객체를 넘겨줄 수 있게 만들어보는 건 여전히 가치가 있다.
    • 아래처럼 그냥 임시 컨테이너의 한 요소의 복사본을 만들고 싶을수도 있기 때문이다.
std::deque<std::string> makeStringDeque();

// makeStringDeque가 돌려준 deque의 다섯 번째 요소의 복사본을 만든다.
auto s = authAndAccess(makeStringDeque(), 5);
  • 함수를 오버로드 할 수도 있겠지만, 관리 이슈가 생긴다.
  • lvalue, rvalue 모두에 바인딩 가능한 참조 매개변수를 authAndAccess에 도입하면 된다.
    • 즉, 이 때 보편 참조를 사용하면 된다.(항목 24 참조)
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i); // 보편 참조를 사용한다.
  • Index는 일단 값 전달로 남겨둔다.
    • 물론 타입을 알 수 없기 때문에 복사로 인한 성능 하락이 심할 수도 있고...
    • 객체 슬라이스 문제가 발생할 수도 있다.(항목 41 참조)
    • 하지만 표준 라이브러리는 operator[]에서 인덱스 매개변수에 값 복사를 채용했다.
  • 보편 참조에는 std::forward를 적용한다.(항목 25 참조)
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) // C++14
{
    authenticateUser();
    return std::forward<Container>(c)[i]; // std::forward 적용
}
  • C++11 컴파일러라면 아래처럼 한다.
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) // C++11
{
    authenticateUser();
    return std::forward<Container>(c)[i]; // std::forward 적용
}

 

 

 

decltype의 예상 밖의 결과
  • 거의 대부분 경험하기 힘들 것이다.
  • 하지만 한 가지 정도는 알아두면 decltype 이해에 도움이 될 것 같다.
  • decltype을 이름에 적용하면, 그 이름에 대해 선언된 타입이 산출된다.
  • 그런데, 이름보다 복잡한 lvalue 표현식에 대해서는 일반적으로 decltype이 항상 lvalue 참조를 산출한다.
    • 즉, 이름이 아니고 형식이 T인 어떤 lvalue 표현식에 대해 decltype은 T&를 산출한다.
  • 어차피 lvalue 표현식은 보통 lvalue 참조를 포함하므로 이 때문에 문제가 생길 일은 거의 없다.
  • 하지만, 아래처럼 미묘한 차이가 생긴다.
int x = 0;

decltype(x); // int이다.
// x는 변수의 이름이기 때문이다.
decltype((x)); // int&이다.
// (x)는 그냥 이름이 아니라 lvalue 표현식이기 때문이다.
  • decltype(auto)를 지원하는 C++14에서는 return문 작성 습관의 미묘한 차이만으로도 리턴 타입 추론이 바뀌는 일이 생길 수 있다.
decltype(auto) f1()
{
    int x = 0;
    ...
    return x; // int 리턴
}

decltype(auto f2()
{
    int x = 0;
    ...
    return (x); // int& 리턴
}
  • 두 번째 것은 지역 변수의 참조를 리턴하게 되므로 크리티컬해진다.
  • 그러므로 decltype(auto)는 아주 조심해서 사용해야 한다.
728x90
Comments