auto in C++
C++ auto 키워드에 대해서
Effective Modern C++ Chapter 2.
auto vs implicit type
변수 초기화
auto
는 변수의 선언과 동시에 초기화를 강제할 수 있다.
1 | int x; // 선언되었지만 초기화되지 않음. |
선언 단순화
auto
는 iterator
같이 복잡한 형식의 선언을 단순화 할 수 있다.
1 | template <typename It> |
Closure 형식 사용
C++에서 closure [1]의 형식은 컴파일러만 알고있기 때문에 명시적으로 선언이 불가능하다. 하지만 auto
를 사용하면 closure형식을 선언할 수 있다.
1 | auto derefUPLess = // derefUPLess는 |
C++14부터는 람다 표현식의 매개변수에도 auto
를 사용할 수 있다.
1 | auto derefUpLess = |
이제 closure의 복사본인 derefUpLess
는 포인터처럼 동작하는 어떤것이든 받을 수 있다.
'왜 굳이 auto
를 이용해서 closure의 복사본을 담는 변수를 선언하는가? std::function
객체를 이용해도 되지 않을까?'라는 생각이 들수도 있지만 std::function
은 auto
에 비해 몇 가지 단점이 존재한다.
std::function
은 C++11에 도입된 함수 포인터 개념을 일반화 한 템플릿이다. 이 템플릿은 함수 포인터 뿐만 아니라 호출 가능한 객체까지 담을 수 있다. std::function
을 선언할 때는 함수포인터와 마찬가지로 함수의 서명(Signature)을 지정해야 한다.
1 | std::function<bool(const std::unique_ptr<Widget>&, |
closure역시 호출 가능한 객체이므로 std::function
에 그 복사본을 담을 수 있다.
1 | std::function<bool(const std::unique_ptr<Widget>&, |
한눈에 보기에도 선언 과정에서 std::function
이 auto
를 사용한 closure변수보다 복잡하다. 하지만 눈에 보이지 않는 더 큰 단점이 존재한다.
auto
를 사용해서 closure 형식의 변수를 선언하면, 그 closure가 사용하는 만큼의 메모리만 사용된다. 그러나 std::function
을 사용하여 closure의 복사본을 담는 변수를 선언하면 그 형식은 std::function
템플릿의 한 인스턴스이며, 그 크기역시 std::function
에 implemetaion에 의해 결정된다. 결과적으로 대부분 closure 객체보다 더 많은 메모리를 사용한다.
그리고 함수의 인라인화를 제한하고 함수 호출 오버헤드때문에, std::function
객체를 통해서 closure를 호출하는 것은 auto
로 선언된 객체를 통해 호출하는 것보다 대부분 느리다.
형식 불일치 예방
auto
의 또다른 장점은 '형식 불일치’를 예방할 수 있다는 점이다.
1 | std::vector<int> v; |
위 코드에서 v.size()
의 반환 형식은 std::vector<int>::size_type
이다. 플랫폼에 따라서 unsigned
와 std::vector<int>::size_type
의 크기가 같을수도, 다를수도 있다. 예를 들어 32비트 Windows에서는 전자는 32비트, 후자는 64비트의 크기를 갖는다. 이때 위 코드는 32비트 변수에 64비트 값을 할당한다는 문제가 발생한다.
auto
를 사용한다면 이런 문제를 피할 수 있다.
1 | auto sz = v.size(); // sz의 형식은 항상 std::vector<int>::size_type과 같다. |
std::unordered_map
같은 해시 테이블을 사용할 때에도 auto
는 실수를 줄여준다.
1 | std::unordered_map<std::string, int> m; |
위 코드에서 해시 테이블에 담긴 std::pair
의 형식은 std::pair<const std::string, int>
이다. 하지만 루프의 변수 p
의 형식은 std::pair<std::string, int>
로 선언되어있다.
따라서 내부적으로 루프를 돌때마다 해시 테이블에 담긴 객체들을 p
의 형식에 맞춰서 임시 객체를 만들고, 객체를 임시객체로 복사한 뒤, p
가 그 임시객체를 참조하게한다. 그리고 루프의 끝에서 이 임시 객체를 파괴한다.
이처럼 실수에의해 의도하지 않은 형식 불일치를 auto
를 통해 사전에 방지할 수 있다.
1 | for(const auto& p : m) |
auto with implicit type
auto
는 가끔 우리눈에 보이지 않는 형식으로 추론되는 경우가 있다. 대표적인 예는 ={}
로 추론되는 std::initialize_list<T>
가 있다. 또 주의해야 하는경우는 Proxy pattern 이 사용되는 경우이다.
std::vector<bool>
의 operator[]
는 bool&
형식이 아닌 내부적으로 선언된 프록시 클래스 std::vector<bool>::reference
형식을 반환한다. 이 반환값을 변수에 저장할 때 명시적으로 bool
형식 변수를 선언하는 경우에는 상관 없지만[2], auto
를 사용하면 의도하지 않은 형식의 변수가 선언된다.
1 | std::vector<bool> v; |
이처럼 auto
가 우리의 의도와는 다르게 추론되는 경우 auto
의 형식 추론을 강제하는 방법을 사용해야 한다.
1 | auto b = static_cast<bool>(v[5]); |
위 코드에서 auto
는 명시적 캐스팅에 의해서 bool
형식을 추론하도록 강제된다.
캐스팅을 통해 auto
의 추론을 강제하는 방식은 코드에 개발자의 의도를 담을 수 있다는 장점도 있다.
1 | double calcEpsilon(); |
calcEpsilon
함수는 double
의 정밀도를 갖는 값을 반환한다.
만약 개발자가 반환 값을 float
의 정밀도로 낮춰서 사용하고 싶다면 float
변수에 그 값을 담으면 된다.
1 | float ep = calcEpsilon(); |
하지만 이 코드는 'calcEpsilon
함수 반환값의 정밀도를 일부러 낮추려고 한다’라는 의도를 명확하게 나타내기 힘들다.
반면 auto
의 추론을 강제하는 방식을 사용하면 이 의도를 명확하게 나타낼 수 있다.
1 | auto ep = static_cast<float>(calcEpsilon()); |