스토리텔링 개발자

[Effective Modern C++] 5. 타입 명시보다 auto 본문

개발/Effective Modern C++

[Effective Modern C++] 5. 타입 명시보다 auto

김디트 2025. 2. 11. 11:02
728x90

항목 5. 명시적 타입 선언보다는 auto를 선호하라

 

 

 

auto를 사용하는게 좋은 상황들
// 1.
int x;
// 깜빡 잊고 초기화하지 않았다!

// 2.
template<typename It>
void dwim(It b, It e)
{
    for(; b != e; ++b)
    {
        typename std::iterator_traits<It>::value_type currValue = *b;
        ...
    }
}
// 반복자가 가리키는 값의 타입이 지나치게 길다.

// 3.
// 클로저의 타입으로 지역 변수를 선언해보자.
// 클로저의 타입은 컴파일러만 알고 있으므로 명시적으로 지정하는 것은 애초에 불가능하다..

 

 

 

해결
// 1.
int x1; // 문맥에 따라 초기화되지 않을 수 있다.
auto x2; // 컴파일 에러
auto x3 = 0; // 초기화를 반드시 챙길 수 있다.

// 2.
template<typename It>
void dwim(It b, It e)
{
    for (; b != e; ++b)
    {
        auto currValue = *b; // 긴~ 타입을 생략할 수 있다.
        ...
    }
}

// 3.
// auto는 타입 추론을 사용한다.(항목 2 참조)
// 그러므로 컴파일러만 알던 타입을 지정할 수 있다.
auto derefUPLess = [](const std::unique_ptr<Widget>& p1,
                      const std::unique_ptr<Widget>& p2)
                      {
                          return *p1 < *p2;
                      };
// C++14에서는 람다 표현식 매개변수에도 auto를 적용할 수 있다.
auto derefUPLess = [](const auto& p1,
                      const auto& p2)
                      {
                          return *p1 < *p2;
                      };

 

 

 

클로저를 담을 땐 std::function 객체를 써도 되지 않을까?
  • std::function
    • 함수 포인터 개념을 일반화한, C++11 표준 라이브러리의 템플릿 중 하나이다.
    • 다만 함수 포인터는 함수만 가리킬 수 있지만, 이 템플릿은 함수처럼 호출할 수 있는 객체면 무엇이든 가리킬 수 있다.
    • 선언 시, 담을 함수의 형식을 반드시 지정해야 한다.
    • std::function<bool(const std::unique_ptr<Widget>&,
                         const std::unique_ptr<Widget>&)> func;
  • 그럼 이제 위의 예시를 auto 대신 std::function 객체를 사용하면 아래와 같아진다.
std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)> derefUPLess = [] (const std::unique_ptr<Widget>& p1,
                                                                      const std::unique_ptr<Widget>& p2)
                                                                      {
                                                                          return *p1 < *p2;
                                                                      };
  • std::function의 문제점
    • 예시를 보면 알 수 있듯, 매우 장황해진다.
    • 대체로 std::function은 auto보다 메모리를 더 많이 소비한다.
      • auto로 선언된 클로저를 담는 변수의 타입은 그 클로저와 완전히 동일하다.
        • 고로 딱 그 만큼의 메모리만 사용한다.
      • std::function으로 선언된 클로저를 담는 변수의 타입은 std::function 템플릿의 한 인스턴스이다.
        • 메모리 크기는 담을 함수의 형식(서명)에 대해 고정되어 있다.
        • 헌데 그 크기가 클로저를 저장하기에 부족할 수도 있다.
        • 그럴 땐 힙 메모리를 할당해서 저장하게 된다.
    • 거의 항상 std::function이 auto보다 느리다.
      • 인라인화(inlining)를 제한하고 간접 함수 호출을 산출하는 구현 세부사항 때문이다.

 

 

 

auto의 장점
  • 변수 초기화 누락을 방지한다.
  • 장황한 변수 선언을 피한다.
  • 클로저를 직접 담을 수 있다.
  • 타입 단축(type shortcut) 문제를 피할 수 있다.
  • 리팩토링(refectoring)이 수월하다.

 

 

 

타입 단축(type shortcut) 문제
std::vector<int> v;
...
unsigned sz = v.size();
// v.size()의 타입은 std::vector<int>::size_type이다.
// 헌데, 그냥 unsigned에 넣어 버린다.
  • 32비트 Windows에서는 unsigned와 std::vector<int>::size_type은 같은 크기이다.
  • 64비트 Windows에서는 unsigned는 32비트지만, std::vector<int>::size_type은 64비트이다.
  • 즉, 문제가 발생할 수 있는 코드이다.
  • 하지만 auto를 사용하면 그냥 해결이다.
std::vector<int> v;
...
auto sz = v.size(); // 타입은 std::vector<int>::size_type

 

 

 

 

auto를 선호하면 좋은 추가 예시
std::unordered_map<std::string, int> m;
...
for(const std::pair<std::string, int>& p : m)
{
    ...
}
  • std::unordered_map의 키는 const이나, 위 코드는 그렇지 않다.
  • 이 경우, 컴파일러는 std::pair<const std::string, int>들을 어떻게든 std::pair<std::string, int>에 우겨넣으려 한다.
    • 즉, 임시 객체를 생성하고,
    • m의 각 객체를 복사하고,
    • 참조 p를 그 임시 객체에 묶는다.
std::unordered_map<std::string, int> m;
...
for(const auto& p : m)
{
    ...
}
  • auto를 사용하면 그냥 해결된다.

 

 

 

auto의 문제점
  • auto 변수의 타입 추론이 예상하는 바와 전혀 다를 경우가 있다.(항목 2, 항목 6 참조)
  • 소스 코드의 가독성(readablility)에 문제가 생긴다.
    • auto는 필수가 아니라 선택이다.
    • IDE 기능으로 완화되는 경우가 많다.
    • 객체 타입을 추상적으로만 파악해도 괜찮은 상황들에선 괜찮다.
      • 이 경우 이름만 잘 지어주면 된다.
728x90
Comments