ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] Type deduction : auto
    C++/Effective Modern C++ 2021. 12. 27. 15:41

    형식 연역 (Type deduction)은 소스코드에서 특정한 코드가 어떤 것으로 바뀌는 것 입니다.

    여기서 '특정한 코드'란 template, auto, decltype 등 이 있으며

    바뀌게 되는 '어떤 것' 에는 자료형부터, 함수, 람다식 등이 있습니다.

     

    이전 글에서, template에 대한 형식 연역을 살펴보았습니다.

    몇 가지 예외를 제외하면, 특정한 규칙에 따라 연역이 직관적으로 이루어짐을 볼 수 있었습니다.

    auto는 template과 유사합니다. 한 가지 경우만 제외하면, template와 동일하게 연역됩니다.

     

    이번 글에서는 auto의 형식 연역에 대하여 살펴보도록 하겠습니다.

     


     

    함수 템플릿은 일반적으로 아래와 같이 표현되고, 호출됩니다.

    //Declaration
    template<typename T>
    void function(ParamType param);
    
    //Call
    function(expr);

    이 때, expr에 대하여 T와 ParamType이 각각 다르게 연역될 수 있었음을 보았습니다.

    auto를 이용한 변수 선언에서 auto는 T와 동일하게, 그리고 형식 지정자 (Type specifier)가 ParamType과 동일하게 취급됩니다.

    예제를 살펴보도록 하겠습니다.

    auto x = 27;
    const auto cx = x;
    const auto& rx = x;

    위와 같은 auto 선언에서, template의 T와 ParamType에 대응되는 요소는 아래 표와 같습니다.

    Declaration T ParamType
    auto x auto auto
    const auto auto const auto
    const auto& auto const auto&

    핵심은 변수가 어떤 형식으로 연역되는지 입니다.

     

    1. 기본적인 형식 연역

     

    더보기

    template의 형식 연역은 세 가지로 구분되어 나뉘게 됩니다.

     

    Case 1. 형식 지정자가 포인터나 참조 형식이지만 보편 참조(Universal reference)는 아닌 경우

    • 포인터나 참조를 무시한 후,  ParamType에 대하여 Pattern-matching방식으로 대응시켜서 T의 형식을 결정합니다.

    Case 2. 형식 지정자가 보편 참조인 경우

    • expr이 l-value일 경우 T와 ParamType 모두 l-value로 연역됩니다.
    • expr이 r-value일 경우 포인터, 참조 형식과 동일한 규칙이 적용됩니다.

    Case 3. 형식 지정자가 포인터도 아니고 참조도 아닌 경우

    • 참조, const, volatile등의 형식 지정자를 무시하여 연역합니다.

    auto의 형식 연역 또한 위의 경우와 유사하게 됩니다.

     

    예제를 보며 각각의 경우를 살펴보도록 하겠습니다.

    auto x = 27;
    const auto cx = x;
    const auto& rx = x;
    auto&& uref1 = x;
    auto&& uref2 = cx;
    auto&& uref3 = 27;

    위의 auto선언에서, 각각의 변수에 대응하는 경우와, 연역되는 형식은 아래와 같습니다.

    Declaration Case Type
    auto x = 27 3 int
    const auto cx = x 3 int
    const auto& rx = x 1 const int
    auto&& uref = x 2 (x는 l-value) int&
    auto&& uref2 = cx 2 (cx는 l-value) const int&
    auto&& uref3 = 27 2 (27은 r-value) int&&

    위의 세 가지 경우 외에도 특별한 경우가 있었습니다.

    배열과 함수가 포인터로 연역되는 경우 입니다.

    예제를 살펴보도록 하겠습니다.

    const char arr[] = "Array";
    auto arr1 = arr;
    auto& arr2 = arr;
    
    void function(int, double);
    auto func1 = function;
    auto& func2 = function;

    위의 배열과 함수, 그리고 그것을 받는 각각의 auto에 대하여 연역되는 형식은 아래와 같습니다.

    Declaration Type   Declaration Type
    auto arr1 = arr const char*   auto func1 = function void (*)(int, double)
    auto& arr2 = arr const char[6]   auto& func2 = function void (&)(int, double)

    위의 예제들에서 볼 수 있듯이, auto의 형식 연역은 템플릿의 형식 연역과 동일하게 작동합니다.

     

    2. 예외 : std::initializer_list<>

     

    더보기

    C++에서 변수를 초기화 할 때에는 아래와 같은 방법을 이용할 수 있습니다.

    int x1 = 27;
    int x2(27);
    int x3 = { 27 };
    int x4{ 27 };

    이 경우, 모든 구문이 명백하게 int입니다.

    하지만, 아래와 같이 int를 auto로 변경할 경우

     

    auto x1 = 27;
    auto x2(27);
    auto x3 = { 27 };
    auto x4{ 27 };

    위 구문은 모두 정상적으로 컴파일 되지만, 자료형은 모두 int가 되지 않습니다.

    위 구문 중 auto x3 = { 27 }; 부분은 int가 아닌 std::initializer_list<>템플릿 인스턴스로 연역됩니다.

    (N3922가 적용되지 않은 C++ 컴파일러는 x4또한 std::initializer_list<>로 연역됩니다.)

    이것이 템플릿과 차이가 존재하는 auto의 형식 연역 규칙입니다.

     

    이제 std::initializer_list<>에 대하여 살펴보도록 하겠습니다.

    std::initializer_list<>는 위 코드의 x3와 같은 상황에서 연역되는 클래스 템플릿 입니다.

    만약 std::initializer_list<>로 연역되지 않는다면, 컴파일이 되지 않습니다.

    대표적인 예시는 다음과 같습니다.

    auto x3 = { 1, 2, 3.0 };

    위의 중괄호 초기치의 값들은 형식이 일관되지 않아 형식 연역이 되지 않습니다.

    여기서 주목할 점은, x3의 형식 연역을 위해 두 종류의 형식 연역이 진행된다는 것 입니다.

    첫 번째로 auto x3가 std::initializer_list<>로 연역되는 것이고

    두 번째로 std::initializer_list<T>의 T가 연역되는 것 입니다.

    위의 예제는 두 번째 형식 연역인 std::initializer_list<T>의 T에 대한 연역이 실패하여 컴파일 되지 않는 것 입니다.

     

    중괄호 초기치가 auto로 연역되는 것과, template으로 연역되는 것은 조금 다릅니다.

    //Declaration
    auto x = { 1, 2, 3 };
    
    template<typename T>
    void function(T param);
    
    //Call
    function({ 1, 2, 3 });
    function(x);

    위와 같은 함수 호출의 경우, x를 인자로 하는 호출은 정상적으로 컴파일 되지만

    중괄호 초기치를 이용한 호출은 컴파일이 되지 않습니다.

    중괄호 초기치를 이용한 호출이 필요하다면, 아래와 같이 파라미터를 변경할 수 있습니다.

    //Declaration
    auto x = { 1, 2, 3 };
    
    template<typename T>
    void function(std::initializer_list<T> param);
    
    //Call
    function({ 1, 2, 3 });
    function(x);

    위와 같이 파라미터를 변경하게 된다면, T는 int로 연역됩니다.

     

    3. 예외 : 함수와 람다식

     

    더보기

    기억해두어야 할 예외가 한가지 더 있습니다.

    C++14부터, 함수의 반환 형식과 람다식의 매개변수에 auto를 사용하는 것이 가능합니다.

    반환형식과 매개변수를 사용자가 아닌 컴파일러가 연역하도록 하는 것 입니다.

     

    그런데 이 때에는, 형식 연역 과정에서 auto가 아닌 템플릿 형식 연역의 규칙이 적용됩니다.

    따라서, 다음과 같은 코드는 컴파일 되지 않습니다.

    //Function
    auto function(){
        return { 1, 2, 3 };
    }
    
    //Lambda
    auto lam = [](auto& val) { };
    lam({ 1, 2, 3 });

    auto를 사용했지만 형식 연역 규칙은 템플릿과 같이 적용되기 때문에, 중괄호 초기치에 대한 형식 연역을 할 수 없는 소스코드 입니다.

     

    이번 글에서는 auto의 형식 연역 과정을, 템플릿과 비교하며 살펴보았습니다.

    템플릿과 거의 동일하지만 몇 가지 다른 부분이 있다는 점에서, 차이점을 확실하게 알아야 할 것입니다.

     

    auto 또한 템플릿과 마찬가지로, 생산성 향상에 도움이 되는 기능입니다.

    하지만 auto를 사용함에 있어서 개발자의 의도와 컴파일러의 해석이 달라지는 일이 없도록, 조심스럽고 확실하게 사용해야 할 것입니다.

    이 글이 auto를 사용함에 있어서 조금이나마 도움이 되었으면 좋겠습니다.

    읽어주셔서 감사합니다.

     

    댓글

Designed by Tistory.