[EMC++] Item 5. 명시적 타입 선언보다는 auto를 선호해라
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
가 얼마나 편리한지 확인할 수 있다.
물론 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) {
}