Item 1 : Understand template type deduction
Type deduction for templates는 modern C++의 auto를 기반으로 한다.
template<typename T>
void f(ParamType param);
f(expr);
위 코드는, 컴파일 시간 동안 T와 ParamType이라는 두가지 타입을 추론하기 위해 expr을 사용한다.
보통 ParamType은 종종 const, reference와 같은 adornment를 포함하기 때문에 두 타입은 자주 다르다.
예를들어, template이 다음과 같이 선언된 경우,
template<typename T>
void f(const T& param);
int x = 0;
f(x);
T는 int로 추론되지만, ParamType은 const int&로 추론된다.
T에 대해 추론된 타입이 함수에 전달된 argument의 타입과 동일할 것으로 예상하는 것은 당연하다.
즉, T는 expr의 타입이다.
위 코드에서 x는 int이므로, T는 int라고 추론된다.
하지만 항상 이런 방식으로 동작하지 않는다.
T에 대한 타입 추론은 expr의 타입뿐만 아니라 ParamType의 형태에 의존한다.
이를 3가지 case로 구분할 수 있다.
Case1: ParamType is a Reference or Pointer, but not a Universal Reference
만약, expr의 타입이 reference이면, reference part를 무시한다.
그런 다음, T를 결정하기 위해, ParamType에 대해 expr의 타입을 패턴 일치 시킨다.
template<typename T>
void f(T& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T is int, param's type is int&
f(cx); // T is const int, param's type is const int&
f(rx); // T is const int, param's type is const int&
위 코드에서 cx와 rx는 const로 지정되었기 때문에, T는 const int로 추론되며 parameter의 타입은 const int&가 된다.
const 객체를 reference parameter로 전달했을 때 callers는 객체가 수정되지 않는 것을 기대한다.
따라서 T& 파라미터를 사용하거는 템플릿에 const 객체를 전달하는 것이 안전하다.
객체의 constness는 T에 대해 추론된 타입의 일부가 된다.
param이 pointer인 경우에도 같은 방식으로 동작한다.
Case2: ParamType is a Universal Reference
만약 expr이 lvalue이면, T와 ParamType은 lvalue references로 추론된다.
템플릿 유형 추론에서 T가 reference로 추론되는 유일한 상황이며 ParamType이 rvalue reference에 대한 구문을 사용하여 선언되었지만 추론된 타입은 lvalue reference이다.
만약 expr이 rvalue이면, Case1의 규칙이 적용된다.
template<typename T>
void f(T&& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(rx); // rx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is rvalue, so T is int, param's type is therefore int&&
Case3: ParamType is Neither a Pointer nor a Reference
pass-by-value를 다루는 경우이다.
expr의 type이 reference이면, reference part를 무시한다.
expr이 const이거나, volatile일 경우에도 무시한다.
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T's and param's types are both int
f(cx); // T's and param's types are agian both int
f(rx); // T's and param's types are still both int
x, cx, rx와 param은 완전히 독립적인 객체이기 때문에, param이 const가 아닌 것은 타당하다.
cx와 rx는 수정될 수 없지만, param은 가능하다.
template<typename T>
void f(T param);
const char* const ptr = "Fun with pointers";
f(ptr);
포인터가 pass-by-value로 전달되는 경우를 살펴보자.
별표 오른쪽에 있는 const는 ptr을 const로 선언한다.
즉, ptr은 다른 위치를 가리키거나 null로 설정할 수 없다.
ptr이 f에 전달되면 포인터를 구성하는 비트가 param에 복사된다.
따라서, 포인터 자체(ptr)는 pass-by-value로 전달된다.
value parameter에 대한 타입 추론 규칙에 따라 ptr의 constness는 무시되고 param에 대해 추론된 유형은 const char*, 즉 const 문자열에 대한 수정 가능한 포인터가 된다.
ptr이 가리키는 것에 대한 constness는 타입 추론중에 유지되지만, ptr 자체의 constness는 복사하여 새 포인터 param을 만들때 무시된다.
Array Arguments
array를 pass-by-value로 전달하면 array 선언은 pointer 선언으로 취급된다.
그러나 template f가 argument를 reference로 가져오도록 하면, 파라미터를 array에 대한 레퍼런스로 선언할 수 있다.
template<typename T>
void f(T& param); // template with by-reference parameter
const char nama[] = "J. P. Briggs";
f(name); // pass array to f
T는 const char[13], f의 파리미터는 const char(&)[13]으로 추론된다.
array를 reference로 선언하면, 템플릿은 array에 포함된 elements의 개수를 추론할 수 있다.
Conclusion
template type deduction을 하는 동안, reference argument는 non-reference로 취급된다. 즉, reference-ness가 무시된다.
universal reference parameter에 대한 타입 추론을 할 때, lvalue argument는 특별하게 취급된다.
by-value parameter에 대한 타입 추론을 할 때, const, volatile argument는 non-const, non-volatile로 취급된다.
template의 타입 추론을 할 때, reference로 초기화하지 않으면, array또는 function names은 pointer로 취급된다.
Item 2: Understand auto type deduction
auto를 사용하여 변수를 선언할 때, auto는 template의 T와 같은 역할을 하고, 변수에 대한 type specifier는 ParamType과 같은 역할을 한다.
auto x = 27; // type specifier for x is auto
const auto cx = x; // type specifier is const auto
const auto& rx // type specifier is const auto&
위 예제에서 x, cx, rx의 타입을 추론하기 위해 컴파일러는 각 선언에 대한 템플릿이 있는 것처럼 작동할 뿐만 아니라 해당 템플릿에 대한 호출이 해당 초기화 식과 함께 이루어진다.
auto에 대한 타입 추론은 한가지만 제외하고 template에 대한 타입 추론과 같다.
따라서, Item1에서 나눈 세가지 case를 적용할 수 있다.
int에 대한 변수를 선언하는 방법은 4가지가 있다.
이때 int대신 auto를 사용하여 변수를 선언하면 int로 선언했을 때와 의미가 달라진다.
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};
auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = {27}; // type is std::initalizer_list<int>, value is {27}
auto x4{ 27 }; // ditto
auto-declared variable에 대한 초기화가 braces를 사용하여 이루어지면, 추론된 타입은 std::initializer_list이다.
만약 braced-initializer안의 값들이 서로 다른 타입이면, 타입을 추론할 수 없으며, 코드는 거부된다.
auto x5 = {1,2,3.0}; //error! can't deduce T for std::initializer_list<T>
x5의 초기화가 중괄호에서 이루어지기 때문에 x5는 std::initalizer_list라고 추론될 수 있다.
std::initializer_list는 템플릿이므로, 어떤 타입 T에 대해 std::initializer_list<T>가 인스턴스화 되어야 한다.
하지만, 중괄호 안에 하나의 타입에 대한 값만 있지 않기 때문에 template type deduction은 실패한다.
braced initalizer는 auto type deduction과 template type deduction에서 다르게 취급된다.
auto-declared variable이 braced initializer로 초기화될 때, 추론된 타입은 std::initializer_list의 instantiation이다.
하지만, 같은 initializer가 대응되는 템플릿에 넘겨졌을 때, type deduction은 실패하며 코드가 거부된다.
auto x = {11,23,9}; // x's type is std::initializer_list<int>
template<typename T> // template with parameter declaration equivalent to x's declaration
void f(T param);
f({11,23,9}); // error! can't deduce type for T
하지만, template의 param을 std::initializer_list<T>로 지정한다면, template type deduction은 T가 무엇인지 추론할 것이다.
template<typename T>
void f(std::initializer_list<T> initList);
f({11,23,9}); // T deduced as int, and initList's type is std::initializer_list<int>
C++14는 함수의 리턴 타입으로 auto를, lambdas에서 auto를 파라미터 선언에 사용하는 것을 허용한다.
그러나 이러한 auto의 사용은 template type deduction을 적용한다.
따라서 auto return type을 사용하는 함수에서 braced initializer를 반환하면 컴파일되지 않을 것이다.
Things to Remember
auto type deduction은 보통 template type deduction과 같다. 하지만, auto type deduction은 braced initializer가 std::initializer_list를 표현한다고 추측하는데 반해, template type deduction은 그렇지 않다.
함수의 return type또는 람다 매개변수의 auto는 auto type deduction이 아니라 template type deduction을 의미한다.
Item3: Understand decltype
container's operator[]의 리턴 타입은 container에 의존한다.
function name앞에 auto를 사용하는 것은 타입 추론과 관계가 없다.
C+11의 trailing return type 문법이 사용된다. 즉, 함수의 리턴 타입은 함수 파리미터 리스트 뒤에 선언된다.
함수의 파라미터가 리턴 타입 지정에 사용될 수 있다는 장점이 있다.
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authenticateUser();
return c[i];
}
authAndAccess처럼 return type을 c와 i를 사용하여 지정할 수 있다.
이러한 선언과 함께, authAndAccess는 operator[]가 반환하는 모든 타입을 반환한다.
C++11은 single-statement lambdas의 리턴 타입의 추론을 허용한다.
그리고 C++14는 이것을 모든 lambdas와 function으로 확장한다.
authAndAccess의 경우 이는 C++14에서 trailing return type을 생략하고 auto만 남길 수 있음을 의미한다.
컴파일러는 함수의 구현으로부터 함수의 리턴 타입을 추론할 것이다.
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) // C++14; not quite correct
{
authenticateUser();
return c[i]; // return type deduced from c[i]
}
Item 1에서 언급했듯이, auto return type specification은 template type deduction을 적용한다.
std::deque<int> d;
authAndAccess(d,5) = 10;
위 코드에서, auto return type deduction은 reference를 제외하기 때문에, authAndAccess의 리턴 타입은 int가 된다.
따라서, rvalue int에 10을 할당할 수 없으므로 코드는 컴파일되지 않는다.
decltype type deduction을 리턴 타입으로 사용하여 authAndAccess가 c[i]의 리턴값과 정확히 같은 타입을 리턴하도록 지정할 수 있다.
타입이 유추되는 동안 decltype 타입 추론 규칙을 사용할 필요가 있을 경우, C++14에서는 decltype(auto) 지정자를 통해 이를 가능하게 한다.
auto는 타입을 추론하여 지정하고, decltype은 추론 동안 decltype 규칙이 사용되도록 한다.
template<typename Container, typename Index> // C++14; works, but still requires refinement
decltype(auto) authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}
decltype(auto)의 사용은 함수의 리턴 타입에 한정되지 않는다.
초기화 표현식에 decltype 타입 추론 규칙을 적용하여 변수를 선언할 때 편리하다.
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction, myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction, myWidget2's type is const Widget&
Things to Remember
decltype은 항상 거의 어떠한 수정 없이 variable이나 expression의 타입을 준다.
이름이 아닌 T 타입의 lvalue 표현식의 경우, decltype은 항상 T& 타입을 보고한다.
C++은 decltype(auto)를 지원한다. auto처럼 그것의 초기화로 부터 타입을 추론한다. 그러나, decltype 규칙을 사용하여 타입 추론을 수행한다.
Item4: Know how to view deduced type
타입 추론 결과를 보기 위한 도구 선택은 정보를 보기 원하는 소프트웨어 개발 프로세스 단계에 의존한다.
세가지 가능성에 대해 살펴본다: 코드를 수정할 때, 컴파일 하는 동안, 런타임에 타입 추론 정보 가져오기.
IDE Editors
Code editiors는 entity에 커서를 올렸을 때, program entities(e.g., variables, parameters, functions, etc.)의 타입을 보여준다.
이것이 작동하려면 코드가 어느 정도 컴파일 가능한 상태여야 한다.
IDE 내부에서 실행되는 컴파일러가 코드를 구문 분석하고 타입 추론을 수행할 만큼 충분히 이해하지 못한다면 추론한 타입을 보여줄 수 없다.
간단한 타입에 대해서는 잘 동작하지만, 복잡한 타입에 대해서는 특별히 도움되지 않을 것이다.
Compiler Diagnostics
컴파일 과정에서 문제를 일으키는 방식으로 오류 메시지를 통해 타입을 확인하는 방법이다.
예를 들어, 선언만 하고 정의하지 않은 템플릿을 타입을 확인하고자 하는 변수를 사용하여 인스턴스화를 시도하면, 컴파일 과정에서 오류가 발생하여, 오류 메시지를 통해 타입을 확인할 수 있다.
Runtime Output
typeid와 std::typeinfo::name을 통해 변수의 추론된 타입을 확인할 수 있다.
객체에서 typeid를 호출하면 std::type_info 객체가 생성되고 std::type_info에는 name이라는 멤버 함수가 있는데, 이는 C스타일 문자열(const char*) 이고 type의 이름을 반환한다.
std::type_info를 사용한 타입 확인은 타입이 값에 의한 매개변수로 템플릿 함수에 전달된 것처럼 처리되도록 지정한다.
따라서 param의 타입이 const Widget * const &인 경우, const Widget*으로 보고된다.
Boost TypeIndex library를 사용하여 정확한 타입을 확인할 수 있다.
boost::typeindex::type_id_with_crv는 types argument를 가지고, const, volatile, reference를 제거하지 않는다.
결과는 boost::typeindex::type_index 객체이며 pretty_name 멤버 함수는 std::string으로 타입을 표현한다.
Things to Remember
타입 추론은 IDE editors, compiler error messages, the Boost TypeIndexlibrary를 사용하여 볼 수 있다.
몇몇 툴의 결과는 정확하지 않을 수도 있으므로 C++의 타입 추론 규칙에 대한 이해는 필수적이다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] Chapter 4: Smart Pointers (0) | 2022.07.26 |
---|---|
[Effective Modern C++] Chapter 3: Moving to Modern C++(1) (0) | 2022.07.20 |
[Effective Modern C++] Chapter 2: Auto (0) | 2022.07.19 |