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가 타입 추론을 어떤 식으로 하는지, 그리고 auto와 decltype(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
c2가 decltype(auto)로 (a)에서 추론됐기 때문에 int& 타입이 돼서, ++c2가 실제 a를 바꾸는 걸 볼 수 있어요.
결함 보고(Defect reports)
이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1265 | C++11 | auto 지정자로 후행 반환 타입을 가진 함수를 선언하고 같은 선언문에서 변수를 정의할 수 있었음 |
금지됨 |
| CWG 1346 | C++11 | 괄호로 감싼 표현식 목록을 auto 변수에 대입할 수 없었음 |
허용됨 |
| CWG 1347 | C++11 | auto 지정자로 타입이 각각 T와 std::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):auto와decltype규칙을 결합한 반환 타입 추론.- 구조화 바인딩:
auto [v, w] = ...형태의 선언.