-
[Effective Modern C++] Type deduction : decltypeC++/Effective Modern C++ 2022. 1. 4. 14:12
형식 연역 (Type deduction)은 소스코드에서 특정한 코드가 어떤 것으로 바뀌는 것 입니다.
여기서 '특정한 코드'란 template, auto, decltype 등 이 있으며
바뀌게 되는 '어떤 것' 에는 자료형부터, 함수, 람다식 등이 있습니다.
이전 두 글에서, template과 auto의 형식 연역 과정을 살펴보았습니다.
두 키워드 모두 몇 가지의 특이사항만 제외한다면 동일한 규칙이 적용됨을 알 수 있었습니다.
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을 반환합니다.
하지만, 위의 예제에서 살펴본 것 처럼 예상하지 않은 결과가 나오는 경우도 있습니다.
그 경우가 세분화 되어있지는 않으니 반드시 숙지하는 것이 좋겠습니다.
읽어주셔서 감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] auto와 std::vector, 그리고 Proxy pattern (0) 2022.02.14 [Effective Modern C++] auto의 사용을 고려해야 할 상황들 (0) 2022.01.10 [Effective Modern C++] Type deduction : typeid, boost/type_index (0) 2022.01.06 [Effective Modern C++] Type deduction : auto (0) 2021.12.27 [Effective Modern C++] Type deduction : Template (0) 2021.12.21