ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] auto의 사용을 고려해야 할 상황들
    C++/Effective Modern C++ 2022. 1. 10. 16:06

    auto는 C++11에서 추가된 변수의 한 종류로써 선언과 동시에 초기화를 해야 하며, 초기화에 사용된 식에 따라 형식이 결정되는 변수입니다.

    이처럼 개념적으로는 매우 간단하지만, 파고들수록 미묘한 구석이 있는 변수입니다.

    초기화에 사용된 식의 형식을 그대로 가져가는 것이 아니라, 특정 규칙에 따라 형식이 결정되어 무턱대고 사용하면 원하지 않는 결과가 나오는 경우도 종종 발생합니다.

    하지만, auto는 사용을 통해 얻는 이득이 명확한 용법입니다.

    이번 글에서는 auto에 대해서 살펴보도록 하겠습니다.

     


     

    1. auto의 선언 및 초기화

     

    더보기

    auto는 선언과 초기화를 동시에 해 주어야 합니다.

    아래와 같이 선언만 했을 경우 컴파일이 되지 않습니다.

    int main() {
        auto a;
        auto b(1);
    }
    C3531 : 'a':형식에 'auto'이(가) 포함된 기호에는 이니셜라이저가 있어야 합니다.

    auto를 사용하면 변수의 초기화를 빼먹는 사소한 실수를 저지를 여지가 사라집니다.

     

    auto는 또한 기본적인 변수 외에도 사용이 가능합니다.

    int main() {
        auto func = [](const auto& val1, const auto& cal2) {return *val1 < *val2; };
    }

    위 func 람다 식은 포인터처럼 작동하는 객체들의 값을 비교하는 함수입니다.

    람다 식의 매개변수에 auto가 사용 가능해진 것은 C++14부터이므로, C++14 이전의 컴파일러는 위 코드가 컴파일 되지 않습니다.

     

    2. auto와 클로저, 그리고 std::function

     

    더보기

    std::function<>은 C++ STL의 한 템플릿으로, 함수 포인터 개념을 일반화 한 객체 입니다.

    일반적으로 함수 포인터는 함수만 가리킬 수 있지만, std::function<>은 호출 가능한 객체이면 무엇이든 가리킬 수 있습니다. 이 때, 가리키는 함수의 형식은 템플릿 매개변수를 통해 지정합니다.

     

    예를 들어 아래와 같은 std::unique_ptr<T>의 비교 함수가 있다고 가정했을 때

    bool(const std::unique_ptr<T>&, const std::unique_ptr<T>&)

    이 함수의 서명에 해당하는 std::function<>객체를 생성할 경우 다음과 같이 됩니다.

    std::function<
        bool(const std::unique_ptr<T>&, const std::unique_ptr<T>&)> func;

    람다 식은 호출 가능한 객체입니다. 따라서 std::function<>객체에 저장 할 수 있습니다.

    따라서 위의 서명을 이용한 람다 식을 하나 선언한다면, 아래와 같이 됩니다.

    std::function<
        bool(const std::unique_ptr<T>&, const std::unique_ptr<T>&)> func
        = [](const std::unique_ptr<T>& val1, const std::unique_ptr<T>& val2)
            { return *val1 < *val2; };

    이것은 위 문단의 auto를 사용해 값을 비교하는 func 람다 식을 auto를 사용하지 않고 선언한 것과 유사합니다.

    차이점이 있다면, 이쪽은 T에만 대응하지만, 위 문단에는 T외의 다른 형식에도 대응할 수 있다는 것 정도입니다.

    이와 같이 auto는 여러 객체에 대한 호환성을 갖출 수 있으며, 타이핑의 양을 줄여 생산성에 도움이 될 수도 있고, 코드의 가독성을 향상시킬 수도 있습니다.

     

    하지만, 그런 것 보다 이번 경우에서는 std::function<>을 사용하는 것 보다 auto를 사용하는 것이 더 좋은 명확한 이유가 존재합니다.

    auto로 선언된 클로저를 담는 변수는 클로저와 같은 형식을 하며, 클로저에 요구되는 만큼의 메모리를 사용합니다.

    하지만 클로저를 담는 std::function<>템플릿 객체는 std::function<>의 인스턴스이며, 이 클래스의 서명에 따라 크기가 고정되어 있습니다. 이 고정된 크기는 클로저를 담기에 부족할 수 있으며, 이 때 std::function<>은 힙 메모리를 할당해서 클로저를 저장합니다. 물론, 이 과정에서 Out of memory 예외가 발생할 수도 있습니다.

    또한, std::function<> 객체는 구현 세부사항에 인라인화(Inlining)와 간접 함수 호출(함수 포인터)을 산출합니다. 이러한 과정으로 인하여 std::function<>은 auto보다 속도 측면에서 느릴 수 있습니다.

     

    요약하자면, 클로저를 담기 위한 변수로써의 auto와 std::function<>객체는 메모리, 시간적 측면에서 auto가 유리합니다.

     

    3. auto와 형식 불일치 (1)

     

    더보기

    간단한 예제를 한번 살펴보도록 하겠습니다.

    std::vector<int> v;
    unsigned sz = v.size();

    vector의 size를 받는 코드입니다.

    vector의 size함수도, unsigned 자료형도 부호 없는 정수이기 때문에 묵시적으로 잘 변환이 됩니다.

     

    이번에는 std::vector의 size함수를 살펴보겠습니다.

    _NODISCARD _CONSTEXPR20 size_type size() const noexcept {
        auto& _My_data = _Mypair._Myval2;
        return static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst);
    }

    반환타입이 size_type 인 것을 볼 수 있는데, 이것은 std::vector<>::size_type으로 선언되있는 코드입니다.

    여기서 중요한 점은 이 std::vector<>::size_type이 운영체제에 따라 크기가 다른 변수라는 것 입니다.

    32비트 WIndows에서는 이 변수가 32비트이지만, 64비트 Windows에서는 64비트입니다.

    그런데 unsigned 변수는 운영체제에 무관하게 32비트입니다.

    이것은 32비트 Windows에서 잘 작동하는 코드가, 64비트에서는 오류가 발생할 수 있다는 의미입니다.

     

    std::vector<int> v;
    auto sz = v.size();

    이 경우 unsigned대신 auto를 사용한다면, auto는 std::vector<>::size_type을 연역할 것이기 때문에 위와 같은 문제는 발생하지 않을 것 입니다.

     

    4. auto와 형식 불일치 (2)

     

    더보기

    간단한 예제를 한번 살펴보도록 하겠습니다.

    std::unordered_map<std::string, int> map;
    	
    for (const std::pair<std::string, int>& p : map) {
        //Do something
    }

    map의 Key - Value 쌍에 대한 특정 연산을 수행하는 코드입니다.

    여기서 주의할 점은 for loop에 지정된 map의 원소쌍에 대한 변수입니다.

    std::unordered_map의 Key는 const입니다. 따라서, 위 코드는 아래와 같이 수정되어야 합니다.

    std::unordered_map<std::string, int> map;
    	
    for (const std::pair<const std::string, int>& p : map) {
        //Do something
    }

    수정하지 않을 경우, 컴파일러는 std::pair<std::string, int>를 std::pair<const std::string, int>로 변환하기 위해 각 반복문마다 임시 객체를 생성하고, map의 각 객체를 복사해 임시 객체에 묶는 연산을 수행합니다. 물론, 이 임시 객체는 반복문의 끝에서 소멸됩니다. map의 각 요소에 대한 참조를 목적으로 한 코드였겠지만, 위와 같은 불필요한 연산이 내부적으로 일어나게 될 수 있습니다.

     

    위와 같은 형식 불일치 또한 auto로 해결 가능합니다.

    std::unordered_map<std::string, int> map;
    	
    for (const auto& p : map) {
        //Do something
    }

    의도치 않은 변환을 막아서 불필요한 연산이 일어나지 않게 할 수도 있고, 타이핑의 양도 줄어듭니다. 또한, 반환 형식에 대해 숙지해야 하는 위와 같은 상황에서의 실수를 방지할 수도 있습니다.

     


     

    auto에 대한 간단한 사용방법과, auto를 사용하면 유리해지거나 이점이 있는 상황들을 살펴보았습니다.

    하지만 auto를 남용할 경우 소스 코드를 보고 형식을 바로 파악하기는 어려워질 수 있거나, auto를 초기화 하는 표현식에 문제가 있어서, auto 자체가 잘못된 형식을 연역할 수도 있습니다.

    특정 상황에서 명시적 선언을 사용할지, auto를 사용해서 형식 추론을 사용할지는 개발자의 판단, 혹은 팀의 규칙에 따라가면 될 것입니다.

     

    읽어주셔서 감사합니다.

     

    댓글

Designed by Tistory.