티스토리 뷰

항목 1에서 템플릿의 형식 연역에 대해서 알아 봤습니다. 템플릿에 대한 형식 연역을 이해하고 있다면 auto의 형식 연역에 대한 것의 거의 전부를 알고 있다고 할 수 있습니다. 한 가지의 기이한 예외를 빼고, auto의 형식 연역이 곧 템플릿의 형식 연역 방식과 같기 때문입니다.

앞서 항목 1 에서는 일반적인 함수 템플릿과 일반적인 템플릿 함수 호출을 다음과 같이 예를 들어서 템플릿 형신 연역을 설명했습니다:

// 템플릿 함수 선언
template<typename T>
void f(ParamType param);

// 템플릿 함수 호출
f(expr);    // 어떤 표현식으로 f 호출

 

auto를 이용해서 변수를 선언할 때 auto는 템플릿의 T 와 동일한 역할을 하며, 변수의 형식 지정자(type specifier)는 ParamType 과 동일한 역할을 합니다. 예제를 통해서 설명하도록 하겠습니다:

auto x = 27;

const auto cx = x;

const auto& rx = x;

 

위의 예제에서 x의 형식 지정자는 auto 그 자체입니다. cx의 경우 형식 지정자는 const auto입니다. 마지막으로 rx의 선언에서는 형식 지정자가 const auto&라고 할 수 있습니다. 이 예들에서 x, cx, rx의 형식들을 연역할 때, 컴파일러는 마치 선언마다 템플릿 함수 하나와 해당 초기화 표현식으로 그 템플릿 함수를 호출하는 구문이 존재하는 것처럼 행동합니다 .

template<typename T>                    // x의 형식을 연역하기 위한 개념적인 템플릿
void func_for_x(T param);

func_for_x(27);                    // 개념적인 호출 : param에 대해 연역된 형식이 바로 x의 형식

template<typename T>                    // cx의 형식을 연역하기 위한 개념적인 템플릿
void func_for_cx(const T param);

func_for_cx(x);                    // 개념적인 호출 : param에 대해 연역된 형식이 곧 cx의 형식

template<typename T>                    // rx의 형식은 역역하기 위한 개념적인 템플릿
void func_for_x(const T& param);

func_for_rx(x);                    // 개념적인 호출 : param에 대해 연역된 형식이 바로 rx의 형식

 

항목 1에서 템플릿 형식 연역을 일반적인 함수 템플릿의 param의 형식 지정자인 ParamType의 특성에 따라 세 가지 경우로 나누어서 이야기 했습니다. auto를 이용한 변수 선언에서는 변수의 형식 지정자가 ParamType의 역할을 하므로, auto 형식 연역 역시 세 가지 경우로 나뉩니다:

  • 경우 1 : 형식 지정자가 포인터나 참조 형식이지만 보편 참조는 아닌 경우
  • 경우 2 : 형식 지정자가 보편 참조인 경우
  • 경우 3 : 형식 지정자가 포인터도 아니고 참조도 아닌 경우'

 

 

 

경우(case) 살펴보기


경우 1과 3의 예는 앞에서 이미 보았습니다.

auto x = 27; // 경우 3
const auto cx = x; // 경우 3
const auto& rx = x; // 경우 1

 

경우 2 역시 기대한 대로 작동합니다.

auto&& uref1 = x;        // x는 int이면서 l-value이므로 uref1의 형식은 int&
auto&& uref2 = cx;        // cx는 const int이면서 l-value이므로 uref2의 형식은 const int&
auto&& uref3 = 27;        // 27은 int이면서 r-value이므로 uref3의 형식은 int&&

 

또한, 항목 1에서 비참조 형식 지정자의 경우 배열과 함수 이름이 포인터로 붕괴하는 것에 대해서 논의하였습니다. auto 역시 그러한 붕괴가 일어납니다.

const char name[] = "R, N. Briggs";    // name의 형식은 const char[13]
auto arr1 = name;                            // arr1의 형식은 const char*
auto& arr2 = name;                            // arr2의 형식은 const char (&)[13]

void someFunc(int, double);                // someFunc의 형식은 void(int, double)
auto func1 = someFunc;                    // func1의 형식은 void(*)(int, double)
auto& func2 = someFunc;                    // func2의 형식은 void(&)(int, double)

 

즉, auto의 형식 연역은 템플릿 형식 연역과 똑같이 작용함을 알 수 있습니다.

 

 

 

기이한 예외


앞서 언급했던 기이한 예외에 대해 다뤄보도록 하겠습니다. 먼저 27을 초기값으로 int 변수를 선언하는 예에서, c++98은 다음 두 가지 구문이 가능합니다.

int x1 = 27;
int x2(27);

 

여기다가 유니폼 초기화(uniform initialization, 중괄호 초기화라고도 합니다)를 지원하는 C++11에서는 위의 두 구문과 더불어 다음과 같은 구문들도 사용가능합니다.

int x3 = {27};
int x4{27};

 

위 모든 경우에서 결과적으로 값이 27인 int가 생긴다는 것은 동일합니다. 그런데, 항목 5에서 설명하겠지만 auto를 이용해서 변수를 선언했을 때 몇 가지 장점이 있습니다. 그러한 점에서 변수 선언들에서 int를 auto로 대체하면 다음과 같은 선언들이 나옵니다.

auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};

 

이 선언들은 모두 문제없이 컴파일되지만, 의미가 달라진 것들이 생깁니다. 처음 두 선언문은 실제로 형식이 int이고 값이 27인 변수를 선언하지만, 나머지 둘은 값이 27인 원소를 하나 담은 std::initializer_list<int> 형식의 변수를 선언합니다 .

auto x1 = 28;        // 형식은 int, 값은 27
auto x2(27);        // 형식은 int, 값은 27
auto x3 = {27};        // 형식은 std::initializer_list<int>, 값은 { 27 }
auto x4{27};        // 형식은 std::initializer_list<int>, 값은 { 27 }

 

이는 auto에 대한 특별한 형식 연역 규칙 떄문입니다. auto로 선언된 변수의 초기치가 중괄호 쌍의 형태이면, 연역된 형식은 std::initializer_list가 됩니다. 만약 다음과 같이 이러한 형식을 연역할 수 없을 때(중괄호 초기치의 값들의 형식이 서로 달라서) 컴파일이 거부됩니다.

2014년 11월에 C++ 표준위원회는 AUTO와 직접 초기화 구문을 이용한 중괄호 초기치에 대한 특별한 형식 연역 규칙을 제거하자는 제안 N3922를 받아들였습니다. 여기서 직접 초기화 구문을 이용한 중괄호 초기치란 중괄호 초기치 앞에 "="가 없는 형태를 말합니다(항목 42 참조). N3922(C++11이나 C++14의 일부는 아니지만, 이를 이미 구현하는 컴파일러들이 존재합니다) 하에서, 위의 예에 나온 x4의 형식은 std::initializer_list<int>가 아니라 int입니다.

auto x5 = {1,2,3.0};    // 컴파일 에러! std::initializer_list<T>의 T를 연역할 수 없습니다.

 

여기서 주목할 점은 두 종류의 형식 연역이 진행된다는 것이다. 첫 번째는 변수 선언에 auto를 사용했다는, 따라서 x5의 형식을 연역해야 한다는 사실에서 비롯되는 형식 연역입니다. 두 번째는 std::initializer_list가 템플릿 형식을 따르며, 따라서 컴파일러가 std::initializer_list<T>에 대한 T의 형식을 연역해야 한다는 것입니다. 위 예에서는 두 번째 종류의 형식 연역이며, 초기치 값들이 달라서 T의 형식 연역에 실패하는 데서 발생하는 문제입니다.

 

auto 형식 연역과 템플릿 형식 연역은 이처럼 중괄호 초기치가 관여할 떄만 차이를 보이게 됩니다. 만약, 템플릿 함수에 동일한 중괄호 초기치를 전달한다면 형식 연역이 실패해서 컴파일이 거부됩니다.

auto x = {11,23,9};            // x의 형식은 std::initializer_list<int>

template<typename T>
void f(T Param);

f({ 11, 23, 9 });                // 오류 ! T에 대한 형식을 연역할 수 없습니다

 

하지만, param의 형식이 어떤 알려지지 않은 T에 대한 std::initializer_list<T>인 템플릿에 그 중괄호 초기치를 전달하면 템플릿 형식 연역 규칙들에 의해 T의 형식이 제대로 연역됩니다.

template<typename T>
void f(std::initiliaer_list<T> initList);

f({11, 23, 9});        // T는 int로 연역되며, initList의 형식은 std::initializer_list<int>로 연역됩니다.

 

 

 

정리


auto의 형식 연역과 템플릿 형식 연역의 실질적인 차이는, auto의 경우에서 중괄호 초기치가 std::initializer_list를 나타낸다고 가정하지만 템플릿 형식 연역은 그렇지 않다는 것 뿐입니다.

앞서 본 중괄호 초기치의 잘못된 형식 연역 탓에 저지르는 전형적인 실수는, 변수를 선언할 때 변수의 원래 의도가 아닌 std::initializer_list로 선언되게 만드는 것입니다. 이러한 함정 떄문에, 꼭 필요한 경우에만 초기치를 중괄호로 감싸는 개발자도 있습니다.

C++14에서는 함수의 반환 형식을 auto로 지정해서 컴파이럴가 연역하게 만들 수 있으며(항목 3 참조), 람다의 매개변수 선언에 auto를 사용하는 것도 가능합니다. 그러나 auto의 그러한 용법들에게는 auto 형식 연역이 아니라 템플릿 형식 연역의 규칙들이 적용됩니다. 그래서 중괄호 초기치를 돌려주는 함수의 반환 형식은 auto로 지정하면 컴파일이 실패합니다.

auto createInitList()
{
    return {1,2,3};            // 오류 ! {1, 2, 3}의 형식을 연역할 수 없습니다
}

 

C++14 람다의 매개변수 형식 명세에 auto를 사용하는 경우에도 마찬가지 이유로 컴파일이 실패합니다.

std::vector<int> v;
...

auto resetV = [&v](const auto& newValue){v = newValue};

...

resetV({1,2,3});        // 오류 ! {1,2,3}의 형식을 연역할 수 없습니다.

 

 

 

기억해 둘 사항들


  • auto 형식 연역은 대체로 템플릿 형식 연역과 같지만, auto 형식 연역은 중괄호 초기치가 std::initializer_list를 나타낸다고 가정하는 반면 템플릿 형식 연역은 그렇지 않다는 차이가 있습니다.
  • 함수의 반환 형식이나 람다 매개변수에 쓰인 auto에 대해서는 auto 형식 연역이 아니라 템플릿 형식 연역이 적용됩니다.

 

 

 

레퍼런스


[Effective Modern C++ - C++11과 C++14를 효과적으로 사용하는 42가지 방법] 항목 2 : auto의 형식 연역 규칙을 숙지하라

 

댓글