Item 5. 명시적 타입 선언보다는 auto를 선호해라


auto를 사용하면 좋은 점

Modern C++에서는 auto 사용을 권장한다. auto는 다음과 같은 장점들을 가지고 있다.

초기화를 잊어버릴 일이 없음


auto는 초기값으로 타입을 추론하기 때문에 반드시 초기화를 해야 한다.

따라서 초기화를 빼먹어서 발생하는 문제를 예방할 수 있다.

int x1;       // 상황에 따라서 초기화가 되지 않을 수 있음
auto x2;      // 컴파일 에러, 초기값이 반드시 필요
auto x3 = 0;  // 0으로 x3 변수를 초기화했으며, int 타입으로 추론됨

길고 복잡한 타입을 간단하게 표현할 수 있음


auto는 길고 복잡한 타입을 간단하게 표현할 수 있다.

반복자(iterator)를 역참조해서 지역 변수를 초기화하는 함수 템플릿을 작성해보자.

template <typename It>
void func(It b, It e) {
  for(; b != e; ++b) {
    typename std::iterator_traits<It>::value_type currentValue = *b;
  }
}

반복자(iterator)가 가리키는 타입을 명시하기 위해서는 템플릿 매개변수에 의존하는 타입임을 나타내는 typename 키워드부터 std::iterator_traits<T> 클래스가 지정한 타입인 value_type까지 써줘야 한다.

이처럼 길고 복잡한 타입 선언을 auto를 사용하면 간단하게 표현할 수 있다.

template <typename It>
void func(It b, It e) {
  for(; b != e; ++b) {
    auto currentValue = *b;
  }
}

컴파일러만 알 수 있는 타입을 지정할 수 있음


auto 키워드를 사용하면 컴파일러만 알 수 있는 타입을 지정할 수 있다.

람다 표현식(lamda expression)을 사용해서 unique_ptr들이 가리키는 string 객체를 비교하는 함수 객체(closure)를 작성해보자.

// C++11
auto derefUPLess = [](const std::unique_ptr<std::string>& p1, const std::unique_ptr<std::string>& p2) {
  return *p1 < *p2;
}

// C++14부터는 람다 표현식의 매개변수에도 auto를 적용할 수 있다.
auto derefUPLess = [](const auto& p1, const auto& p2) {
  return *p1 < *p2;
}

이때 컴파일러에서 추론하는 타입을 보면 auto가 얼마나 편리한지 확인할 수 있다. image

물론 auto 대신 호출 가능한 모든 객체를 담을 수 있는 std::function 객체 타입을 사용하여 클로저를 표현할 수도 있다.

클로저(closure) : 람다 표현식(익명 함수)이 생성하는 객체 인스턴스 (캡처된 변수도 포함)

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을 사용하면 리턴 타입과 매개 변수 타입을 명시해야 하는 것은 물론, auto를 사용하는 것보다 속도와 메모리 측면에서 단점이 존재한다.

auto를 사용하는 변수는 클로저와 같은 타입이므로 필요한 크기의 메모리만 사용한다. 그러나 std::function을 사용하는 변수는 고정된 메모리 크기를 사용하여 더 많은 메모리를 소비하며, 만약 클로저가 고정된 메모리 크기보다 크다면 heap 메모리에 저장해야 한다.

또한, std::function은 구현 세부 사항들(간접 함수 호출, 인라인화 제한)이 함수 호출을 느리게 만들어 auto를 사용하는 것보다 호출 속도가 느리다.

타입 단축 문제를 피할 수 있음 (플랫폼 이식성이 좋음)


auto를 사용하면 의도치 않게 타입 크기가 줄어드는 타입 단축(type shortcut) 문제를 피할 수 있다.

std::vector<int> v;

// v.size()의 리턴 타입은 std::vector<int>::size_type 이다.
unsigned sz = v.size(); 

vector<int>::size 함수의 리턴 값을 받기 위해서 unsigned 타입을 사용하는 예를 흔히 볼 수 있다.

하지만 vector<int>::size 함수의 정확한 리턴 타입은 std::vector<int>::size_type 이다.

32비트 Windows에서는 std::vector<int>::size_type 타입의 크기가 unsigned 타입과 같은 32bit 이지만 64비트 Windows에서는 std::vector<int>::size_type 타입의 크기가 64bit로 unsigned 타입보다 크다.

즉, unsigned 타입을 사용하면 32bit 운영체제에서는 잘 동작하는 코드가 64bit 운영체제에서는 타입 단축으로 인해 오작동할 수 있다.

auto를 사용하면 이러한 문제를 간단하게 해결할 수 있다.

std::vector<int> v;

auto sz = v.size();

실수 할 수 있는 타입 선언을 줄여줌


auto은 실수할 수 있는 타입 선언을 줄여준다.

std::unordered_map을 사용해서 해시 테이블을 구현해보자.

std::unordered_map<std::string, int> m;

해시 테이블의 각 요소들은 std::string 타입의 key와 int 타입의 value를 가진 std::pair 형태로 표현된다.

이때 주의할 점은 std::pair의 형태가 std::pair<std::string, int>가 아니라 변하지 않는 key 값에 const 키워드를 붙인 std::pair<const std::string, int> 형태라는 것이다.

만약 해시 테이블의 요소들을 순회하려고 할 때, 명시적 타입을 잘못 선언하면 실제 객체 값을 복사한 임시 객체가 생성되어 효율이 떨어질 수 있다.

// 실제 요소 타입과 달라서 실제 객체가 아닌 임시 객체가 생성됨
for(const std::pair<std::string, int>& e : m) {
}

auto를 사용하면 이러한 실수를 방지할 수 있다.

// auto는 std::pair<const std::string, int>로 정확하게 추론됨
// 임시 객체를 생성하지 않고 실제 객체에 접근
for(const auto& e : m) {
}


카테고리:

업데이트: