콘텐츠로 이동

타입 추론(auto 포함) (Type Deduction (including auto))

타입 추론은 프로젝트에 익숙하지 않은 독자에게 코드를 더 명확하게 만들거나 코드를 더 안전하게 만들 때만 사용하세요. 단지 명시적인 타입을 적는 불편을 피하려고 사용하지는 마세요.

정의:

C++에는 코드에 타입을 명시적으로 적는 대신 컴파일러가 타입을 추론하도록 허용하거나, 심지어 요구하는 맥락이 여럿 있습니다.

함수 템플릿 인수 추론

함수 템플릿은 템플릿 인수를 명시하지 않고 호출할 수 있습니다. 컴파일러가 함수 인수의 타입에서 그 인수들을 추론합니다.

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

f(0);  // f<int>(0)을 호출합니다

람다 표현식도 템플릿 매개변수를 가질 수 있으며, 같은 방식으로 추론됩니다.

// `vec`를 내림차순으로 정렬합니다
std::sort(vec.begin(), vec.end(), []<typename T>(T lhs, T rhs) { return lhs > rhs; });

auto 변수 선언

변수 선언에서 타입 자리에 auto 키워드를 쓸 수 있습니다. 컴파일러는 변수의 초기화식에서 타입을 추론하며, 같은 초기화식에 대한 함수 템플릿 인수 추론과 동일한 규칙을 따릅니다(괄호 대신 중괄호를 쓰지 않는 한).

auto a = 42;  // a는 int입니다
auto& b = a;  // b는 int&입니다
auto c = b;   // c는 int입니다
auto d{42};   // d는 std::initializer_list<int>가 아니라 int입니다

auto에는 const를 붙일 수 있고, 포인터나 참조 타입의 일부로 쓸 수 있으며, (C++17부터는) 타입이 아닌 템플릿 인수로도 쓸 수 있습니다. 이 문법의 드문 변형으로 auto 대신 decltype(auto)를 쓰기도 하는데, 이 경우 추론되는 타입은 초기화식에 decltype을 적용한 결과입니다.

함수 반환 타입 추론

함수 반환 타입 자리에도 auto(그리고 decltype(auto))를 쓸 수 있습니다. 컴파일러는 함수 본문의 return 문에서 반환 타입을 추론하며, 변수 선언과 같은 규칙을 따릅니다.

auto f() { return 0; }  // f의 반환 타입은 int입니다

람다 표현식의 반환 타입도 같은 방식으로 추론될 수 있지만, 이때는 명시적인 auto가 아니라 반환 타입을 생략하는 것으로 촉발됩니다. 혼란스럽게도 함수의 후행 반환 타입 문법 역시 반환 타입 자리에 auto를 쓰지만, 그것은 타입 추론에 의존하지 않습니다. 명시적인 반환 타입을 적는 또 다른 문법일 뿐입니다.

auto 매개변수

함수나 람다 표현식은 매개변수 타입 하나 이상을 auto 키워드로 대신할 수 있습니다. 그러면 그것은 일반 함수가 아니라 함수 템플릿이 되며, auto 함수 매개변수마다 별도의 템플릿 매개변수가 생깁니다.

// `vec`를 내림차순으로 정렬합니다
std::sort(vec.begin(), vec.end(), [](auto lhs, auto rhs) { return lhs > rhs; });

람다 init 캡처

람다 캡처에는 명시적인 초기화식을 붙일 수 있으며, 이를 통해 기존 변수를 캡처하는 데 그치지 않고 완전히 새로운 변수를 선언할 수 있습니다.

[x = 42, y = "foo"] { ... }  // x는 int이고, y는 const char*입니다

이 문법으로는 타입을 지정할 수 없습니다. 대신 auto 변수의 규칙으로 추론됩니다.

클래스 템플릿 인수 추론

아래를 참조하세요.

구조적 바인딩

auto를 써서 튜플, 구조체, 배열을 선언할 때 객체 전체에 이름을 붙이는 대신 개별 원소마다 이름을 지정할 수 있습니다. 이 이름들을 "구조적 바인딩"이라 하고, 선언 전체를 "구조적 바인딩 선언"이라고 합니다. 이 문법에서는 감싸는 객체의 타입도, 개별 이름의 타입도 지정할 방법이 없습니다.

auto [iter, success] = my_map.insert({key, value});
if (!success) {
  iter->second = value;
}

auto에는 const, &, &&를 붙일 수도 있지만, 이 한정자들은 엄밀히 말해 개별 바인딩이 아니라 익명의 튜플·구조체·배열에 적용된다는 점에 유의하세요. 바인딩의 타입을 결정하는 규칙은 상당히 복잡합니다. 결과가 놀라운 경우는 드물지만, 선언이 참조를 선언하더라도 바인딩 타입은 보통 참조가 아니라는 점은 예외입니다(그래도 대개 참조처럼 동작하기는 합니다).

(이 요약들은 많은 세부 사항과 주의점을 생략한 것입니다. 자세한 내용은 링크를 참조하세요.)

장점:

  • C++ 타입 이름은 길고 번거로울 수 있으며, 템플릿이나 네임스페이스가 얽히면 특히 그렇습니다.
  • 하나의 선언이나 좁은 코드 영역 안에서 같은 C++ 타입 이름이 반복될 때, 그 반복이 가독성에 도움이 되지 않을 수 있습니다.
  • 타입을 추론에 맡기는 편이 더 안전할 때도 있습니다. 의도치 않은 복사나 타입 변환의 가능성을 피할 수 있기 때문입니다.

단점:

C++ 코드는 대개 타입이 명시적일 때 더 명확하며, 타입 추론이 코드의 먼 곳에 있는 정보에 의존할 때 특히 그렇습니다. 다음과 같은 표현식에서

auto foo = x.add_foo();
auto i = y.Find(key);

y의 타입이 잘 알려져 있지 않거나 y가 여러 줄 위에서 선언되었다면, 결과 타입이 무엇인지 분명하지 않을 수 있습니다.

프로그래머는 타입 추론이 언제 참조 타입을 만들어 내고 언제 그러지 않는지를 이해해야 합니다. 그러지 않으면 의도하지 않은 복사가 생깁니다.

추론된 타입이 인터페이스의 일부로 쓰인다면, 프로그래머가 값만 바꾸려는 의도로 타입을 바꿔 버려서 의도한 것보다 훨씬 큰 API 변경이 일어날 수 있습니다.

결정:

기본 규칙은 이렇습니다. 타입 추론은 코드를 더 명확하거나 더 안전하게 만들 때만 사용하고, 단지 명시적인 타입을 적는 불편을 피하려고 사용하지는 마세요. 코드가 더 명확해지는지 판단할 때는 독자가 반드시 여러분의 팀 사람이거나 프로젝트에 익숙한 사람이 아니라는 점을 기억하세요. 여러분과 리뷰어에게는 불필요한 군더더기로 느껴지는 타입이 다른 사람에게는 유용한 정보를 주는 경우가 아주 많습니다. 예를 들어 make_unique<Foo>()의 반환 타입은 명백하다고 볼 수 있지만, MyWidgetFactory()의 반환 타입은 아마 그렇지 않을 것입니다.

이 원칙은 모든 형태의 타입 추론에 적용되지만, 세부 사항은 아래 절들에서 설명하는 것처럼 다릅니다.

함수 템플릿 인수 추론

함수 템플릿 인수 추론은 거의 언제나 괜찮습니다. 타입 추론은 함수 템플릿을 다루는 기본 방식으로 기대되는 것인데, 그 덕분에 함수 템플릿이 무한한 일반 함수 오버로드 집합처럼 동작할 수 있기 때문입니다. 따라서 함수 템플릿은 거의 항상 템플릿 인수 추론이 명확하고 안전하거나, 아니면 아예 컴파일되지 않도록 설계됩니다.

지역 변수 타입 추론

지역 변수라면 명백하거나 무관한 타입 정보를 없애서 코드를 더 명확하게 만드는 데 타입 추론을 쓸 수 있습니다. 그러면 독자가 코드의 의미 있는 부분에 집중할 수 있습니다.

std::unique_ptr<WidgetWithBellsAndWhistles> widget =
    std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
absl::flat_hash_map<std::string,
                    std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
    it = my_map_.find(key);
std::array<int, 6> numbers = {4, 8, 15, 16, 23, 42};
auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
auto it = my_map_.find(key);
std::array numbers = {4, 8, 15, 16, 23, 42};

타입에는 위 예의 it처럼 유용한 정보와 상용구가 섞여 있는 경우가 있습니다. 그것이 반복자라는 사실은 명백하고 많은 맥락에서 컨테이너 타입이나 키 타입은 무관하지만, 값의 타입은 아마 유용할 것입니다. 이런 상황에서는 관련 있는 정보를 전달하는 명시적 타입으로 지역 변수를 정의할 수 있는 경우가 많습니다.

if (auto it = my_map_.find(key); it != my_map_.end()) {
  WidgetWithBellsAndWhistles& widget = *it->second;
  // `widget`으로 무언가 합니다
}

타입이 템플릿 인스턴스이고 그 인수들은 상용구인데 템플릿 자체는 정보를 준다면, 클래스 템플릿 인수 추론으로 상용구를 감출 수 있습니다. 다만 이것이 실제로 의미 있는 이득을 주는 경우는 꽤 드뭅니다. 클래스 템플릿 인수 추론에는 별도의 스타일 규칙도 적용된다는 점에 유의하세요.

map 같은 컨테이너의 원소에 명시적 타입을 적는 것은 틀리기 쉽습니다. 그런 경우에는 키와 값에 대한 구조적 바인딩이 쌍 하나를 나타내는 변수 하나보다 거의 언제나 더 명확합니다.

더 단순한 선택지로 해결되는 상황이라면 decltype(auto)를 쓰지 마세요. 꽤 생소한 기능이라 코드 명확성에 드는 비용이 큽니다.

변수 선언에 auto를 언제 쓸지에 대한 더 자세한 내용은 TotW #232를 참조하세요.

반환 타입 추론

반환 타입 추론은(함수와 람다 모두) 함수 본문에 return 문이 아주 적고 그 밖의 코드도 거의 없을 때만 사용하세요. 그렇지 않으면 독자가 반환 타입이 무엇인지 한눈에 알 수 없기 때문입니다. 또한 함수나 람다의 범위가 아주 좁을 때만 사용하세요. 반환 타입을 추론하는 함수는 추상화 경계를 정의하지 않기 때문입니다. 구현이 곧 인터페이스가 됩니다. 특히 헤더 파일의 공개 함수는 반환 타입을 추론하게 두는 일이 거의 없어야 합니다.

함수 매개변수 타입 추론

매개변수에 구체적인 타입을 줄 수 있다면 매개변수 타입 추론(auto를 쓰든 템플릿 매개변수를 쓰든)은 피하세요. 한 가지 타입으로만 호출될 것이라면 그 타입을 명시하는 편이 거의 언제나 더 명확합니다. 정의된 곳 바로 근처에서 호출되어 독자가 양쪽을 쉽게 함께 볼 수 있거나, 아주 잘 알려진 인터페이스에 넘기는 람다여서 결국 어떤 타입으로 호출될지가 명백한 경우(예: 위의 std::sort 예)는 예외입니다.

람다가 아닌 함수에서는 auto 매개변수를 쓰지 마세요. 대신 이름 있는 템플릿 매개변수를 쓰세요. 그 함수가 템플릿이라는 사실을 더 분명하게 드러내기 때문입니다.

void F(auto arg) { ... }
template <typename T>
void F(T arg) { ... }

람다 표현식에서 매개변수의 타입을 참조해야 한다면, 그 타입을 템플릿 매개변수로 만드는 편이 낫습니다.

[](auto x) { decltype(x) y; ... }
[]<typename T>(T x) { T y; ... }

람다 init 캡처

init 캡처는 더 구체적인 스타일 규칙이 다루며, 그 규칙이 타입 추론의 일반 규칙을 대체합니다.

구조적 바인딩

다른 형태의 타입 추론과 달리, 구조적 바인딩은 더 큰 객체의 원소에 의미 있는 이름을 붙여 줌으로써 독자에게 실제로 정보를 더해 줄 수 있습니다. 그래서 auto라면 그렇지 않을 상황에서도 구조적 바인딩 선언은 명시적 타입보다 가독성 면에서 순이득을 줄 수 있습니다. 객체가 pair나 tuple일 때(위의 insert 예처럼) 특히 유익한데, 애초에 그것들에는 의미 있는 필드 이름이 없기 때문입니다. 다만 insert 같은 기존 API가 강제하지 않는 한 일반적으로 pair나 tuple을 쓰지 않아야 한다는 점에 유의하세요.

std::map<K, V> 같은 컨테이너의 원소는 구조적 바인딩의 이득을 특히 크게 봅니다. 쌍의 두 부분에 더 구체적인 이름을 줄 수 있을 뿐 아니라, 타입 추론을 쓰면 원소의 키 타입에 const를 붙이지 않는 흔한 버그를 피할 수 있습니다. 그 버그는 의도치 않은 복사를 끌어들이면서 놀랍게도 컴파일에 성공하는 경우가 많습니다.

바인딩하는 객체가 구조체라면 여러분의 용도에 더 잘 맞는 이름을 붙이는 것이 도움이 될 때도 있습니다. 다만 그 이름이 독자에게는 원래 필드 이름보다 덜 익숙할 수 있다는 점도 염두에 두세요. 바인딩 이름이 원래 필드 이름과 다르다면, 함수 매개변수 주석과 같은 문법으로 원래 필드 이름을 주석에 적기를 권장합니다.

auto [/*field_name1=*/bound_name1, /*field_name2=*/bound_name2] = ...

함수 매개변수 주석과 마찬가지로, 이렇게 하면 도구가 필드 순서를 잘못 적은 것을 잡아낼 수 있습니다.


옮긴이 풀이

핵심: 더 명확하거나 더 안전할 때만 타입 추론

auto 같은 타입 추론은 코드를 더 명확하거나 더 안전하게 만들 때만 쓰세요. 단지 긴 타입을 적기 귀찮아서 쓰면 안 됩니다. 명확한지 판단할 때의 독자는 "팀 밖의, 프로젝트에 익숙하지 않은 사람"임을 기억하세요.

// 좋음: 타입이 상용구라 auto가 더 깔끔
auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
auto it = my_map_.find(key);

// 주의: 결과 타입이 한눈에 안 보이면 명시가 낫다
auto i = y.Find(key);  // y가 멀리 선언됐다면 불명확

형태별 지침

  • 함수 템플릿 인수 추론: 거의 항상 OK.
  • 지역 변수 auto: 상용구 타입을 지우고 의미 있는 부분에 집중시킬 때 좋음. 단 참조/복사 여부에 주의.
  • 반환 타입 추론: 함수 본문이 매우 짧고 범위가 좁을 때만. 헤더의 공개 함수는 거의 추론하지 마세요(구현이 인터페이스가 됨).
  • 매개변수 추론: 람다가 아닌 함수에서 auto 매개변수 대신 이름 있는 템플릿 매개변수를 쓰세요(템플릿임이 드러나도록).
  • decltype(auto): 더 단순한 방법이 되면 쓰지 마세요.

구조적 바인딩은 예외적으로 권장

구조적 바인딩(structured binding)은 요소에 의미 있는 이름을 주므로 auto와 달리 정보를 더합니다. 특히 std::map의 원소나 pair/tuple에서 유용하며, 키 타입의 const 누락 같은 버그도 막아줍니다.

for (const auto& [key, value] : my_map) { ... }