2017년 2월 23일 목요일

auto Type Deduction (auto 타입 추론)

 이번 글은 C++11에 새롭게 추가된 auto 키워드에 관해 이야기해 보려고 한다. auto라는 단어가 뜻하는 것 처럼 누군가 자동으로 무엇인가를 해줄 것이라는 걸 예상할 것이다. 그렇다. auto 키워드는 타입이 들어갈 자리에 대신 들어가서 (템플릿에서 그랬던 것처럼) 컴파일러가 주변 코드를 이용해서 타입을 대신 결정하도록 하는 키워드이다. Effective Modern C++에서도 역시 심도있게 다루고 있으며 그 내용을 기반으로 내가 이해하는 바를 여기서 기록하고자 한다.

 auto가 수행하는 작업이 곧 템플릿 인자의 타입을 결정하는 것과 거의 같기 때문에 템플릿 타입 추론에 관해서 잘 모르는 상태라면 이번 글을 읽기 전에 이전에 올린 "템플릿 타입 추론"에 관한 글을 읽고 이해해 두는 것이 좋을 것 같다.

 그럼 먼저 auto의 타입 추론과 템플릿의 타입 추론이 얼마나 같은지(사실 약간 다르다) 간단한 설명을 보도록 하자. 먼저 템플릿 함수의 전형적인 예는 아래와 같다.
template<typename T>
void f(T param);

f(expr); // expr은 임의의 표현식
 일반적인 auto 선언문은 아래와 같은 형태이다.
auto x = expr; // expr은 임의의 표현식
 컴파일러는 템플릿 함수 f()에 넘기는 인자 expr의 타입에 기초하여 T의 타입 추론을 수행한다. 이 때는 "템플릿 타입 추론" 방식으로 동작하게 되고 그 동작에 대한 설명은 여기를 참조하기 바란다. 자 그 다음 auto의 예를 보면 바로 느낌이 올 것이다. auto에서도 역시 expr의 타입에 기초하여 타입 추론을 수행하는데 그 규칙은 템플릿의 그것과 동일하다. 즉, auto가 템플릿의 T역할을 하는 것이다.

 간단히 몇 가지 비교를 해보면 이해가 더 쉬울 것이다. 아래 예를 보자.
template<typename T>
void f1(T param);

template<typename T>
void f2(T& param);

template<typename T>
void f3(const T& param);

template<typename T>
void f4(T&& param);

auto a1 = expr;        // f1(expr)과 같은 타입 추론 수행
auto& a2 = expr;       // f2(expr)과 같은 타입 추론 수행
const auto& a3 = expr; // f3(expr)과 같은 타입 추론 수행
auto&& a4 = expr;      // f4(expr)과 같은 타입 추론 수행
 위 예에서 auto와 T의 역할을 비교해보면 auto가 "템플릿 타입 추론" 방식으로 결정된다는 게 별로 이상한 일이 아님을 알 수 있을 것이다. 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는 function
                            // 타입은 void(int, double) 
auto func1 = someFunc; // func1의 타입은 void (*)(int, double)
auto& func2 = someFunc; // func2의 타입은 void (&)(int, double)
 템플릿 타입 추론에서 비-참조형과 참조형의 인자에 함수나 배열을 넘겼을 때의 동작은 auto 타입 추론에서도 그대로 적용된다. 즉 비-참조형 인자에 함수나 배열을 넘기면 포인터형으로 바뀌어 추론된다. 이 정도면 "auto 타입 추론"은 "템플릿 타입 추론"과 같다고 할 만 하다.

 하지만 auto는 템플릿 타입 추론과 한 가지 다른 점이 있는데 바로 braced initializer가 초기화값으로 주어졌을 때이다. 아래 예를 보면 auto의 타입이 예상과 다르게 결정되는 걸 볼 수 있다.
int x1 = 27;
int x2(27);
int x3 = { 27 }; // C++11, uniform initialization
int x4{ 27 }; // C++11, uniform initialization

auto a1 = 27; // 타입은 int, 값은 27
auto a2(27);  // 타입은 int, 값은 27
auto a3 = { 27 }; // 타입은 std::initializer_list<int>, 값은 {27}
auto a4{ 27 };    // 타입은 std::initializer_list<int>, 값은 {27}
 C++11에서 소개된 uniform initialization 덕분에 위의 x1, x2, x3, x4가 모두 가능한 선언이다. 하지만 똑같은 방식으로 auto 선언문으로 바꾸면 결과는 달라진다. 주석에서 볼 수 있듯이 a3, a4는 int형이 아니고 std::initializer_list<int>형으로 추론되기 때문이다. braced initializer를 이미 사용해 본 적이 있다면 컴파일러가 상황에 맞춰서 그 값들을 std::initializer_list형으로 생성한다는 걸 이미 알고 있을 것이다. auto 선언문에서도 컴파일러는 braced initializer를 std::initializer_list형으로 바꿔서 타입 추론을 수행한다. 컴파일러의 그런 동작을 이미 알고 있었다면 어느 정도 이해가 되는 상황일 것이다.

 위와 같이 auto는 braced initializer를 std::initializer_list형으로 인식하고 타입 추론을 수행하지만 템플릿에 braced initializer를 넘기면 타입 추론을 하지 못하고 컴파일에 실패한다. 이 점이 두 타입 추론의 차이점이다. 즉 아래 예에서 f()의 호출은 실패한다.
// auto 타입 추론
auto x = { 11, 23, 9 }; // x의 타입은 std::initializer_list형으로 추론됨

// 템플릿 타입 추론
template
void f(T param);

f({ 11, 23, 9 }); // error! 타입 추론을 할 수 없음.

 C++14에서는 한 가지 더 짚고 가야할 것이 있다. C++14부터는 auto를 함수의 return 타입과 람다의 인자에서도 사용이 가능한데 이 때는 braced initializer를 std::initializer_list형으로 인식하지 않고 컴파일을 실패하게 된다. 즉 아래 두 가지 경우에 대해서는 (auto 타입 추론이 아닌) "템플릿 타입 추론"을 수행한다.
auto createInitList() {
    return { 1, 2, 3 }; // error: { 1, 2, 3 }을 타입 추론할 수 없음.
}
std::vector<int> v;
...

auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
...

resetV({ 1, 2, 3 }); // error! { 1, 2, 3 }을 타입 추론할 수 없음.
 위의 두 예에서 모두 auto는 에러가 난다. "템플릿 타입 추론"과 똑같이 braced initializer를 통해서 타입 추론을 하지 못하는 것을 볼 수 있다. 이렇게 보면 auto가 타입 추론되는 방식은 변수를 선언할 때만 braced initializer를 인식(std::initializer_list)한다는 점을 제외하면 템플릿 타입 추론과 완전히 동일하다.

 마지막으로 간단히 요약해 보자면...
  1. auto 타입 추론은 템플릿 타입 추론과 거의 같지만 braced initializer를 std::initializer_list형으로 인식해서 추론을 수행한다는 점이 다르다. (템플릿 타입 추론은 braced initializer를 통해서는 타입 추론을 실패함)
  2. auto가 함수의 return 타입, 람다의 인자에 사용되었을 때는 템플릿 타입 추론과 완전히 동일하게 동작한다.(즉 braced initializer를 통한 타입 추론은 실패한다)

0 개의 댓글:

댓글 쓰기