[Effective Modern C++] Chapter 1. 형식 연역 (Type Deduction) [항목 1~4]

Chapter 1. 형식 연역 (Type Deduction)

항목 1. 템플릿 형식 연역 규칙을 숙지하라.

아래에 적어놓은 코드를 보면 C++11/14에서의 Template Type Deduction 규칙을 파악할 수 있을 것이다.
대부분 직관과 거의 잘 맞아떨어진다.
그래도 주의 할 점 몇가지를 살펴보면, 일단 첫번째는 함수와 배열의 decay 부분이다.
일반적으로 C에서 배열은 포인터로 붕괴되고, 함수도 포인터로 붕괴된다.
C++에서도 이는 마찬가지인데, 주의할 점이 참조(&) 가 사용될 때는 붕괴가 되지 않는다.
따라서 template type deduction에 있어서도 ParamType이 &일 경우에는 배열과 함수가 decay되지 않는다.
그리고 두번째로는 보편 참조(universal reference) 이다.
구체적인 것들은 아래 코드를 열심히 보면 이해가 될 것이다.
#include <utility>

template <typename T>
void func_ref(/*ParamType*/ T &) {}

template <typename T>
void func_ptr(/*ParamType*/ T *) {}

template <typename T>
void func_unv_ref(/*ParamType*/ T &&) {}

template <typename T>
void func_val(/*ParamType*/ T) {}

template <typename T, std::size_t N>
void func_arr_ref(/*ParamType*/ T (&)[N]) {}

int main()
{
    /**********************************************/
    const volatile int a = 3;

    func_ref(a);  // T -> const volatile int
                  // ParamType -> const volatile int &

    func_ptr(&a);  // T -> const volatile int
                   // ParamType -> const volatile int *

    func_unv_ref(a);  // T -> const volatile int &
                      // ParamType -> const volatile int &

    func_unv_ref(std::move(a));  // T -> const volatile int
                                 // ParamType -> const volatile int &&

    func_val(a);  // T -> int
                  // ParamType -> int

    /**********************************************/
    const char * const str = "hello";

    func_ref(str);  // T -> const char * const
                    // ParamType -> const char * const &

    func_ptr(str);  // T -> const char
                    // ParamType -> const char *

    func_val(str);  // T -> const char *
                    // ParamType -> const char *

    /**********************************************/
    const int arr[10] = {};

    func_ref(arr);  // T -> const int [10]
                    // ParamType -> const int (&)[10]
                    // *** An array doesn't decay ***

    func_ptr(arr);  // T -> const int
                    // ParamType -> const int *
                    // An array decays to pointer.

    func_val(arr);  // T -> const int *
                    // ParamType -> const int *

    func_arr_ref(arr);  // T -> const int
                        // ParamType -> const int (&)[10]

    /**********************************************/
    void junk();

    func_ref(junk);   // T -> void ()
                      // ParamType -> void (&)()
                      // *** A function doesn't decay ***

    //func_ref(&junk); // COMPILE ERROR!!
                       // The reason is that a function doesn't decay in this case.

    func_ptr(junk);   // T -> void ()
                      // ParamType -> void (*)()
                      // A function decays to a pointer.

    func_ptr(&junk);  // Same to above.

    func_val(&junk);  // T -> void (*)()
                      // ParamType -> void (*)()
}

void junk() {}

[기억해 둘 사항들]
 - 템플릿 형식 연역 도중에 참조 형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
 - 보편 참조 매개변수에 대한 형식 연역 과정에서 왼값 인수들은 특별하게 취급된다.
 - 값 전달 방식의 매개변수에 대한 형식 연역 과정에서 const 또는 volatile(또는 그 둘 다인) 인수는 비 const, 비 volatile 인수로 취급된다.
 - 템플릿 형식 연역 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴한다. 단, 그런 인수가 참조를 초기화하는 데 쓰이는 경우에는 포인터로 붕괴하지 않는다.

항목 2. auto의 형식 연역 규칙을 숙지하라.

C++11부터 추가된 auto의 Type Deduction Rule은 기본적으로 template의 type deduction rule과 같다.
const auto & var = ...;
가 있을 때, auto를 템플릿의 T로 const auto &를 템플릿의 ParamType으로 생각하면 기본적으로 대부분 맞다.
그러나, 몇 가지 예외사항들과 주의해야할 점들이 있다.
아래 코드에서 그 점들을 다루고 있다.
코드를 보면 되지만, 직접 설명을 좀 하자면,,
일단, std::initializer_list<auto> 같은 것은 안된다. 기본적으로 auto가 template과 type deduction rule이 거의 똑같다고 해도, 저런식으로 auto를 사용하는 것은 표준에 없다.
그리고 중괄호 초기치에 대한 auto의 특별한 형식 연역 규칙을 주의해야 한다. (auto a = {1,2,3} 같은...)
이런 규칙은 template에서는 허용되지 않는데 특별하게 auto에서 허용되는 규칙이다.
그리고 또, auto x(1); 은 int로, auto x = {1} 와 auto x{1}은 std::initializer_list<int>로 연역되는 점을 주의해야 한다. 그러나 직접 초기화 구문을 이용한 중괄호 초기치에 대해 관련된 특별규칙을 없애자는 제안 N3922가 2014년 11월에 받아들여졌고, C++17에 최종적으로 반영되었다. (참고 : http://stackoverflow.com/questions/25612262/why-does-auto-x3-deduce-an-initializer-list)
어쨌든 C++11에서 이런 예외사항이 있는데, C++14에 추가된 "람다의 매개변수 선언에 사용되는 auto"와 "함수의 반환 형식에 사용되는 auto"의 경우에는 이러한 예외사항이 적용되지 않는다. (즉, 이 경우들에서는 auto가 template의 형식 연역 규칙을 따른다고 봐야한다.)
어쨌든, 정리하자면 중괄호 초기치와 관련된 부분을 제외하면 auto의 형식 연역 규칙은 template의 것과 완전히 같다.
#include <initializer_list>

template <typename T>
void func_val(/*ParamType*/ T) {}

template <typename T>
void func_initlist(/*ParamType*/ std::initializer_list<T>) {}

int main()
{
    /***************** Exceptional Case in C++11 *****************/

    // func_val({ 1,2,3 });  // COMPILE ERROR!
    
    func_initlist({ 1,2,3 });  // T -> int
                               // ParamType -> std::initializer_list<int>

    auto a = { 1,2,3 };  // auto -> std::initializer_list<int>
                         // Type of Variable -> std::initializer_list<int>

    // std::initializer_list<auto> a = { 1,2,3 };  // There is no standard for it.

    /***************** Exceptional Case in C++14 *****************/

    auto lambda_func = [](auto) {};
    // lambda_func({ 1,2,3 });  // COMPILE ERROR!
}

auto exceptional_case_in_cpp14()
{
    // return { 1,2,3 };  // COMPILE ERROR!
}

[기억해 둘 사항들]
 - auto 형식 연역은 대체로 템플릿 형식 연역과 같지만, auto 형식 연역은 중괄호 초기치가 std::initializer_list를 나타낸다고 가정하는 반면 템플릿 형식 연역은 그렇지 않다는 차이가 있다.
 - 함수의 반환 형식이나 람다 매개변수에 쓰인 auto에 대해서는 auto 형식 연역이 아니라 템플릿 형식 연역이 적용된다.

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

decltype은 거의 항상 변수나 표현식의 형식을 아무 수정 없이 보고한다.
그러나 아래와 같이 약간 주의해야 할 점들도 있다.
// int func_1();
decltype(auto) func_1() { int a = 1; return a; }

// int& func_2();
decltype(auto) func_2() { int a = 1; return (a); }

template <typename T>
class Example
{
public:
    Example(const T& param) : m_var(param) {}
private:
    T m_var;
};

int main()
{
    /* Usage of "decltype" */

    auto var_1 = func_1();

    decltype(var_1) var_2;

    //Example ex(var_2);  // COMPILE ERROR!
                          // Maybe it is possible since C++17.
    Example<decltype(var_2)> ex(var_2);
}

decltype은 위 코드의 경우와 같이 종종 쓰이게 된다.
특히 위 코드에서 Example 클래스의 객체를 생성할 때 같은 경우 C++14에서는 class constructor에 대해 template type deduction이 적용안되기 때문에 위와 같이 많이 사용하게 된다. 그러나 C++17에서 아마 이 부분이 가능해 질 것으로 보이므로 더 이상 이런 용법의 사용은 안하게 될 것 같다. (http://en.cppreference.com/w/cpp/language/class_template_deduction)

[기억해 둘 사항들]
 - decltype은 항상 변수나 표현식의 형식을 아무 수정 없이 보고한다.
 - decltype은 형식이 T이고 이름이 아닌 왼값 표현식에 대해서는 항상 T& 형식을 보고한다.
 - C++14는 declytype(auto)를 지원한다. decltype(auto)는 auto처럼 초기치로부터 형식을 연역하지만, 그 형식 연역 과정에서 decltype의 규칙들을 적용한다.

항목 4. 연역된 형식을 파악하는 방법을 알아두라.

내 경험 상 (Visual Studio 2015기준) 보통의 경우에는 IDE 편집기에서 마우스 커서를 갖다 대면, 연역된 형식을 파악할 수 있다. 그러나 표현식이나 형식이 좀 복잡해지면, 그리고 연역이 탬플릿 내에서 되는 경우 IDE가 형식을 알려주지 못하는 경우가 많다.

예를 들면, 이런식으로 형식을 불완전(?)하게 띄어준다.. 그나마 이 경우는 눈으로 파악이 가능한 경우,,, 실제로는 이 보다 더 복잡한 경우도 많다.
그러나 컴파일러는 정확하게 형식을 알려주기 때문에 나는 이럴 때 일부러 연역된 형식을 알고 싶은 부분에 컴파일 에러를 만들어서 연역된 형식을 컴파일러가 띄워주도록 유도하기도 한다.
아니면 책에서 소개해준대로 Boost.TypeIndex 라이브러리를 사용하는 것도 방법이 될 수 있다.
하지만, 나는 지금껏 이렇게까지 해서 연역된 형식을 파악할 필요성을 느낀 적은 없다. 기본적인 C++ type deduction rule의 숙지 & IDE의 지원 정도면 충분한 것 같다.

[기억해 둘 사항들]
 - 컴파일러가 연역하는 형식을 IDE 편집기나 컴파일러 오류 메시지, Boost TypeIndex 라이브러리를 이용해서 파악할 수 있는 경우가 많다.
 - 일부 도구의 결과는 유용하지도 않고 정확하지도 않을 수 있으므로, C++의 형식 연역 규칙들을 제대로 이해하는 것은 여전히 필요한 일이다.



Effective Modern C++ 첫 포스팅 끝!

댓글