auto

auto (자리표시자 타입)

타입을 일일이 써 주기 번거로울 때, C++는 **자리표시자 타입(placeholder type)**이라는 걸 제공해요. 자리표시자 타입 지정자는 나중에 바뀔 타입을 나타내는데, 보통 초기화식으로부터 추론(deduction)해서 그 자리를 채워요. 그 대표적인 키워드가 auto예요.

출처: cppreference

본문

문법

type-constraint(optional) auto (1)
type-constraint(optional) decltype(auto) (2) (C++14부터)
자리 의미
type-constraint (C++20부터) 개념(concept) 이름. 선택적으로 한정될 수 있고, 선택적으로 <>로 감싼 템플릿 인자 목록이 뒤따를 수 있어요.

자리표시자 auto에는 const& 같은 수식자가 함께 올 수 있고, 이 수식자들은 타입 추론에 참여해요. 반면 자리표시자 decltype(auto)는 선언된 타입의 유일한 구성요소여야 해요. (C++14부터)

type-constraint가 있으면, 자리표시자에 대해 추론된 타입을 T라 할 때 type-constraint가 다음과 같이 제약 표현식(constraint expression)을 도입해요. type-constraint가 Concept<A₁, ..., Aₙ>이면 제약 표현식은 Concept<T, A₁, ..., Aₙ>이 되고, 아니면(type-constraint가 인자 목록 없는 Concept이면) 제약 표현식은 Concept<T>가 돼요. 제약 표현식이 유효하지 않거나 false를 반환하면 추론이 실패해요. (C++20부터)

쓸 수 있는 문맥(Explanation)

자리표시자 타입 지정자는 다음 문맥에서 나타날 수 있어요.

  • 매개변수 선언: 아래 매개변수 선언들에서 선언되는 매개변수의 타입은 문법 (1) 형태일 수 있어요. 람다 표현식의 매개변수가 자리표시자 타입을 가지면 그 람다 표현식은 제네릭 람다(generic lambda)예요. (C++14부터)
  • 비타입 템플릿 매개변수가 자리표시자 타입을 가지면, 그 타입은 대응하는 템플릿 인자로부터 추론돼요. (C++17부터)
  • 함수 선언의 매개변수가 자리표시자 타입을 가지면 **축약 함수 템플릿(abbreviated function template)**이 선언돼요. (C++20부터)

함수 선언

자리표시자 타입은 후행 반환 타입(trailing return type)을 포함하는 함수 선언자의 선언 지정자에 나타날 수 있어요.

자리표시자 타입은 함수 선언자의 선언된 반환 타입의 선언 지정자나 타입 지정자에 나타날 수 있어요. 이 경우 반환 타입 추론(return type deduction)이 적용돼요. (C++14부터)

auto f() -> int; // OK: f는 int를 반환
auto g() { return 0.0; } // C++14부터 OK: g는 double을 반환
auto h(); // C++14부터 OK: h의 반환 타입은 정의될 때 추론됨

변수 선언

자리표시자 타입으로 선언된 변수의 타입은 그 초기화식으로부터 추론돼요. 이 사용은 변수의 초기화 선언(initializing declaration)에서 허용돼요.

자리표시자 타입은 선언 지정자 시퀀스 안의 선언 지정자 중 하나로만, 또는 그런 선언 지정자를 대체하는 타입을 지정하는 후행 반환 타입의 타입 지정자 중 하나로만 나타날 수 있어요. 이 경우 그 선언은 변수를 하나 이상 선언해야 하고, 각 변수는 비어 있지 않은 초기화식을 가져야 해요.

// 선언 지정자 안의 "auto"들
auto x = 5; // OK: x는 int 타입
const auto *v = &x, u = 6; // OK: v는 const int*, u는 const int 타입
static auto y = 0.0; // OK: y는 double 타입

auto f() -> int;
auto (*fp)() -> auto = f; // OK: 후행 반환 타입의 "auto"는
                          // f로부터 추론될 수 있음

auto 지정자는 구조화 바인딩(structured binding) 선언에도 쓸 수 있어요. (C++17부터)

new 표현식

자리표시자 타입은 new 표현식의 type-id 타입 지정자 시퀀스에 쓸 수 있어요. 그런 type-id에서 자리표시자 타입은 타입 지정자 시퀀스 안의 타입 지정자 중 하나로, 또는 그런 타입 지정자를 대체하는 타입을 지정하는 후행 반환 타입으로 나타나야 해요.

auto 타입 지정자는 함수 스타일 캐스트(function-style cast)의 타입 지정자로 쓸 수 있어요. (C++23부터)

주의할 점(Notes)

C++11 이전까지 auto저장 기간(storage duration) 지정자라는 의미를 가졌어요.

위에 명시하지 않은 문맥에서 자리표시자 타입을 사용하는 프로그램은 잘못된 형태(ill-formed)예요.

선언이 여러 실체를 선언하고 선언 지정자 시퀀스가 자리표시자 타입을 사용하면, 다음 조건 중 하나라도 만족하면 프로그램은 잘못된 형태예요.

  • 선언된 실체 중 일부가 변수가 아닌 경우.
  • 자리표시자 타입을 대체하는 타입이 각 추론에서 같지 않은 경우.
auto f() -> int, i = 0; // Error: "auto"로 함수와 변수를 함께 선언
auto a = 5, b = {1, 2}; // Error: "auto"의 타입이 서로 다름

auto 키워드는 중첩 이름 지정자(nested name specifier)에도 쓸 수 있어요. auto:: 형태의 중첩 이름 지정자는 제한된 타입 자리표시자 추론(constrained type placeholder deduction) 규칙에 따라 클래스나 열거 타입으로 대체되는 자리표시자예요. (concepts TS)

기능 테스트 매크로 표준 기능
__cpp_decltype_auto 201304L (C++14) decltype(auto)

예제

여기서는 auto가 타입 추론을 어떤 식으로 하는지, 그리고 autodecltype(auto)가 참조를 다룰 때 어떻게 달라지는지를 확인해 볼게요.

#include <iostream>
#include <utility>

template<class T, class U>
auto add(T t, U u) { return t + u; } // 반환 타입은 operator+(T, U)의 타입

// 함수 호출의 완벽 전달(perfect forwarding)은 decltype(auto)를 써야 해요
// 호출한 함수가 참조로 반환하는 경우 때문이에요
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args) 
{ 
    return fun(std::forward<Args>(args)...); 
}

template<auto n> // C++17 auto 매개변수 선언
auto f() -> std::pair<decltype(n), decltype(n)> // auto는 brace-init-list에서 추론 불가
{
    return {n, n};
}

int main()
{
    auto a = 1 + 2;          // a의 타입은 int
    auto b = add(1, 1.2);    // b의 타입은 double
    static_assert(std::is_same_v<decltype(a), int>);
    static_assert(std::is_same_v<decltype(b), double>);
    
    auto c0 = a;             // c0의 타입은 int, a의 복사본을 보유
    decltype(auto) c1 = a;   // c1의 타입은 int, a의 복사본을 보유
    decltype(auto) c2 = (a); // c2의 타입은 int&, a의 별칭
    std::cout << "before modification through c2, a = " << a << '\n';
    ++c2;
    std::cout << " after modification through c2, a = " << a << '\n';
    
    auto [v, w] = f<0>(); // 구조화 바인딩 선언
    
    auto d = {1, 2}; // OK: d의 타입은 std::initializer_list<int>
    auto n = {5};    // OK: n의 타입은 std::initializer_list<int>
//  auto e{1, 2};    // DR n3922 이후 Error, 그 이전엔 std::initializer_list<int>
    auto m{5};       // OK: DR n3922 이후 m의 타입은 int, 이전엔 initializer_list<int>
//  decltype(auto) z = { 1, 2 } // Error: {1, 2}는 표현식이 아님
    
    // auto는 람다 표현식처럼 이름 없는 타입에 자주 쓰여요
    auto lambda = [](int x) { return x + 3; };
    
//  auto int x; // C++98에선 유효, C++11부터 error
//  auto x;     // C에선 유효, C++에선 error
    
    [](...){}(c0, c1, v, w, d, n, m, lambda); // "unused variable" 경고 억제
}

가능한 출력:

before modification through c2, a = 3
 after modification through c2, a = 4

c2decltype(auto)(a)에서 추론됐기 때문에 int& 타입이 돼서, ++c2가 실제 a를 바꾸는 걸 볼 수 있어요.

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 1265 C++11 auto 지정자로 후행 반환 타입을 가진 함수를 선언하고 같은 선언문에서 변수를 정의할 수 있었음 금지됨
CWG 1346 C++11 괄호로 감싼 표현식 목록을 auto 변수에 대입할 수 없었음 허용됨
CWG 1347 C++11 auto 지정자로 타입이 각각 Tstd::initializer_list<T>인 두 변수를 정의할 수 있었음 금지됨
CWG 1852 C++14 decltype(auto)auto 지정자도 자리표시자였음 이 경우엔 자리표시자가 아님
CWG 1892 C++11 함수 포인터 type-id의 반환 타입이 auto일 수 있었음 금지됨
CWG 2476 C++11 CWG 1892 해결로 함수 포인터 변수의 반환 타입을 초기화식에서 추론하는 것이 금지됨 허용됨

더 알아보기

  • decltype: 표현식의 타입을 얻는 또 다른 도구.
  • decltype(auto): autodecltype 규칙을 결합한 반환 타입 추론.
  • 구조화 바인딩: auto [v, w] = ... 형태의 선언.