본문 바로가기
컴퓨터/Modern Effective C++ 정리

Modern Effective C++ Item 6 : auto로 안되면 명타초 쓰자

by 김짱쌤 2015. 3. 10.

tem 6 : auto가 안되면 명타초를 쓰자.

auto의 빈틈 : 특수한 벡터 std::vector<bool>

std::vector<T>는 확장가능한 배열 템플릿 클래스로
임의의 타입 T에 대한 연속적인 배열을 만들어준다.
그런데 T가 bool 인 경우 메모리 낭비를 막기 위하여
특수하게 다른 형태로 구현된다.
std::vector<bool>은 각 원소들을 하나의 비트로 표현하고
연속된 비트로 bool의 배열을 만들어 준다.

일반적인 벡터 std::vector<T>의 operator[]의 리턴 형식은 T&이다.
그러나 C++ 에서 하나의 비트에 대한 참조가 불가능하기 때문에
일반적인 방법으로는 배열의 원소에 접근하는 것이 어렵다.
이 해결을 위해 마치 bool&처럼 작동하는 프록시 오브젝트
std::vector<bool>::reference가 사용된다.

std::vector<bool>::reference의 동작

   //예제 코드
   std::vector<bool> features(const Widget& w); //std::vector<bool>을 리턴하는 함수
   ...
   Widget w;
   bool highPriority1 = features(w)[5]; //bool을 활용하는 경우
   auto highPriority2 = features(w)[5]; //auto를 활용하는 경우
   ...
   processWidget(w, highPriority1);     //함수에 적용
   processWidget(w, highPriority2);  

highPriority1은 features(w)[5]에 의해 초기화 된다.
먼저 std::vector<bool>타입인 features(w)operator[]가 호출되고
std::vector<bool>::reference를 리턴하고 이는 bool타입 변수를 초기화 하기위하여
features(w)의 5번째 비트에 해당되는 값이 bool로 형변환되어 저장된다.
이런 과정을 거친 highPriority1는 잘 저장된 bool 변수로 아래 함수에서도 잘 동작한다.

highPriority2역시 features(w)[5]에 의해 초기화된다.
하지만 auto이므로 바로 std::vector<bool>::reference로 타입 추론한다.
std::vector<bool>::reference는 참조되는 비트의 주소에 offset값을 더한 주소를 가르킨다.

features(w)에서 리턴되는 임시 std::vector<bool>를 temp라고 하자.
temp가 가지고 있는 연속된 비트의 첫 주소를 0x0001이라 하면,
temp[5]가 리턴하는 std::vector<bool>::reference
0x0005를 가르키는 포인터를 가지게 된다.

문제는 features(w)가 리턴한 temp가 rvalue라는 점이다.
대입 연산 이후에 temp는 파괴되기 때문에,
highPriority2가 갖는 포인터가 댕글링 상태가 된다.
따라서 processWidget(w, highPriority2);함수 호출의 결과가 undefined behavior가 된다.

Proxy Class

  • 프록시 오브젝트(클래스)란?

다른 타입의 동작을 흉내내거나 보강하는데 사용되는 우렁각시(유틸리티).
라이브러리의 유저(프로그래머)가 사용하기 편하게 만들어준다.
위의 std::vector::reference나 STL의 스마트 포인터등이 이에 속한다.
std::shared_ptr등의 눈에 보이는 형태의 프록시 오브젝트가 있지만,
대부분 유저가 그 존재를 몰라도 잘 동작할 수 있게 하는 경우가 많다.
ex: std::vector<bool>::referencestd::bitsetstd::bitset::reference

  • 눈에 보이지 않는 형태의 프록시 오브젝트는 auto랑 안맞는다.

invisible한 프록시는 한 명령문 내에서 동작하고 사라지는 형태로 설계되어 있다.
이런 프록시 오브젝트를 auto를 사용하여 따로 저장하는 것은
처음 설계의 규칙을 위반하는 것으로 문제를 일으키기 쉽다.
위의 std::vector<bool>::reference의 undefined behavior 문제가 그 예시가 된다.

   auto var = invisible proxy class type temp; //이런 소리를 안나게 하라!
  • "invisible" 프록시 오브젝트를 알아보는법

위에서 설명했듯이, auto를 제대로 쓰려면
API의 리턴값이 눈에 보이지 않는 프록시 오브젝트인지 아닌지를 알아야 한다.
기본적으로 라이브러리의 문서에 프록시 오브젝트들을 명시하는 경우가 많이 있다.
문서에서 설명이 잘 안되어 있다면, 헤더파일을 보면 된다.

namespace std 
{ 
   template <class Allocator>
   class vector<bool, Allocator> //std::vector<bool>의 헤더 부분
   {
   public:class reference { … };
      reference operator[](size_type n);
      …
   };
}

원래 std::vector<T>[]가 T&를 리턴하는데,
위 예제의 경우는 새로운 클래스인 reference를 리턴하고 있다.
이런 이상한(unconventional) 경우가 프록시 오브젝트가 사용되고 있다는 것을 말해준다.
사용하는 라이브러리의 인터페이스를 주의깊게 보면,
어디에 프록시 오브젝트가 쓰이는지 알 수 있다.

invisible proxy와 auto 같이 쓰는 법 - 명타초

이럴때 auto를 아예 안쓰는 것보다 좋은 방법이 있다.
auto가 원하는 타입을 제대로 추론을 못한다면,
추론할 수 있게 프록시를 형변환 해주면된다.
이걸 명시적 타입 초기화(명타초)라고 한다.

   auto highPriority = static_cast<bool>(features(w)[5]);

위와 같이 auto를 사용하면, features(w)[5]에서 std::vector<bool>::reference가 리턴되고
static_cast에 의해 비트의 5번째 값을 bool로 형변환시킨다.
그 후 auto의 타입 추론이 동작하여 highPriority는 bool로 값을 복사하고 저장된다.
이러면 undefined behavior가 생길 여지가 없다. 해피 엔딩.

Things To Remember

  • invisible proxy타입을 auto로 추론하면 초기화하기 적절하지 않은 타입이 나올 수 있다.
  • 명시적 타입 초기화 방식은 auto를 원하는 타입으로 추론하는 것을 강제한다.