C++ Type deduction
C++ 에서의 형식 연역
Effective Modern C++ Chapter 1.
Type deduction in template
1 | template <typename T> |
위 코드는 컴파일 단계에서 expr
로 부터 T
와 ParamType
에 대한 형식 연역이 이루어진다. 이때 T
의 형식 연역은 expr
과 ParamType
에 영향을 받는다.
ParamType이 l-value ref 인 경우
1 | template <typename T> |
ParamType
이 참조형식이므로 T
의 형식 연역 과정에서 expr
의 참조는 무시된다.
만약 expr
이 const
성질을 갖는다면 T
역시 const
의 성질을 갖는다. 따라서 ParamType
이 T&
인 함수에 const
객체를 전달해도 안전하다(객체의 상수성이 보장됨).
1 | template <typename T> |
ParamType
이 const
로 선언되었다면 참조와 마찬가지로 T
의 형식 연역 과정에서 expr
의 const
가 무시된다.
ParamType이 r-value ref인 경우
ParamType
이 T
에 대한 r-value reference(T&&
)지만, 형태만 그럴뿐 expr
에 따라 r-value reference와 다르게 동작할 수도 있다.
1 | template <typename T> |
expr
이 l-value라면 T
와 ParamType
이 모두 l-value reference로 연역된다. 템플릿 형식 연역에서 유일하게 T
가 참조로 연역된다.
1 | ... |
expr
이 r-value라면 첫 번째 경우의 규칙대로 T
에 대한 형식 연역이 이루어진다.
ParamType이 참조가 아닌 경우
1 | template <typename T> |
param
은 expr
의 복사본이 전달(pass-by-value)되기 때문에 expr
과 param
은 서로 독립적인 객체이다. expr
이 수정될 수 없는 const
라도 그 복사본인 param
은 상관없다. 따라서 T
의 형식 연역 과정에서 expr
의 참조, const, volatile 은 무시된다.
1 | const char* const ptr = "Fun with pointers"; |
위 코드에서 expr
은 수정할 수 없는 문자(const char)열 을 가리키는 수정할 수 없는 포인터 (* const)이다. 당연하게도 T
의 형식 연역 과정에서 ptr
이 가리키는 대상의 상수성은 변함 없지만 ptr
자체의 상수성은 무시된다.
배열 인수
배열은 포인터와 다르지만 포인터로 decay 될 수 있다. 이 과정에서 배열 길이에 대한 정보를 잃는다.
1 | const char name[] = "J. P. briggs"; // name의 형식은 const char[13] |
C++에는 배열 형식의 매개변수 라는 것은 없다. void foo(int param[]);
같은 선언은 void foo(int* param);
의 또다른 표현방식일 뿐 매개변수가 배열이라는것을 보장하지 않는다. 템플릿 형식 연역 과정에서도 T
는 포인터로 연역된다.
함수의 매개변수를 배열로 선언할 수는 없지만, 배열에대한 참조로 선언할 수는 있다.
1 | template <typename T> |
위 코드처럼 ParamType
이 참조이고 expr
이 배열이라면, T
는 const char [13]
으로 연역되고 ParamType
은 그 참조인 const char (&)[13]
으로 연역된다.
배열 참조는 배열의 길이정보를 그대로 갖고있다. 이를 이용하면 배열의 길이를 연역하는 템플릿을 만들 수 있다.
1 | template <typename T, std::size_t N> |
constexpr
을 이용하면 함수의 결과를 컴파일 타임에 이용할 수 있다.
함수 인수
배열처럼 함수역시 포인터로 decay 된다.
1 | template <typename T> |
Type deduction in auto
auto의 형식 연역은 템플릿에서의 형식 연역과 대부분 비슷하다. auto
는 템플릿 형식 연역과정에서 T
와 동일한 역할을 하며, 변수의 형식 지정자(type specifier)은 ParamType
과 동일한 역할을 한다.
1 | auto x = 27; // x의 형식 지정자는 auto |
형식 지정자가 ParamType
과 동일한 역할을 하므로, 위 코드는 각각의 ParamType
을 갖는 템플릿처럼 행동한다.
1 | template <typename T> |
즉 auto
도 예외 하나를 제외하면 템플릿 형식 연역과 동일하다.
템플릿 형식 연역과 동일한 경우
템플릿 형식 연역을 세 가지 경우로 나눈것 처럼 auto
의 형식 연역도 세 가지로 나뉜다.
- 경우 1: 형식 지정자가 l-value ref인 경우
- 경우 2: 형식 지정자가 r-value ref인 경우
- 경우 3: 형식 지정자가 참조가 아닌경우
경우 1과 3
1 | auto x = 27; // 경우 3 auto는 int |
경우 2
1 | ... |
배열, 함수의 decay 역시 동일하게 동작한다.
1 | const char name[] = "R. N. Briggs"; |
템플릿 형식 연역과 다른 경우
C++11에서는 네 가지 방법으로 27을 초기값으로 갖는 int
형 변수를 선언할 수 있다.
1 | int x1 = 27; |
위 네 가지 구문의 결과는 모두 값이 27인 int
형 변수가 생성되는 것으로 동일하다. 하지만 int
대신 auto
를 사용하면 그 결과가 다르다.
1 | auto x1 = 27; |
x1
, x2
, x4
는 모두 값이 27인 int
형 변수로 동일하지만, x3
는 값이 27인 원소를 하나 가지고있는 std::initilaize_list<int>
형식의 변수가 된다. auto
에 대한 형식 연역에는 'auto
로 선언된 변수의 initializer가 ={}
의 형태이면 std::initialize_list
로 연역된다’라는 특수가 규칙이 존재한다. std::initilaize_list<T>
는 템플릿이므로 이 과정에서 T
에 대한 형식 연역이 발생한다. 따라서 모든 원소의 타입이 동일해야한다.
1 | auto x5 = { 1, 2, 3.0 }; // error! std::initilaize_list<T>의 T를 연역할 수 없음 |
한가지 주의할 점은 중괄호를 이용한 std::initialize_list
생성은 auto
의 형식 연역에서만 발생한다는 점이다.
1 | template <typename T> |
C++14 에서는 함수의 반환 형식과 람다의 매개변수에 auto
를 사용할 수 있다. 하지만 이 경우에는 auto
형식 연역 규칙이 아닌 템플릿 형식 연역 규칙이 적용된다.
1 | auto createInitList() |
1 | std::vector<int> v; |
템플릿 형식 연역에서는 중괄호가 std::initialize_list
로 연역되지 않으므로 위 코드는 컴파일 에러가 발생한다.
decltype
decltype
은 템플릿이나 auto
와 달리 주어진 이름이나 표현식의 구체적인 형식을 수정없이 그대로 말해준다.
1 | const int i = 0; // decltype(i) : const int |
decltype의 산출 방식
- 인자가 식별자(이름)인 경우: 선언된 형식 그대로 산출
- 인자가 표현식(expression)인 경우
- 표현식이 pure r-value인 경우: 표현식 결과 형식 그대로(
T
) 산출 - 표현식이 l-value인 경우: 표현식 결과 형식의 l-value reference(
T&
)로 산출 - 표현식이 x-value인 경우: 표현식 결과 형식의 r-value reference(
T&&
)로 산출
- 표현식이 pure r-value인 경우: 표현식 결과 형식 그대로(
1 | int i = 17; |
decltype의 활용
C++11부터는 함수의 반환 형식을 연역할 수 있지만, 몇 가지 제약이 따른다.
1 | template <typename Container, typename Index> |
위 코드처럼 decltype
을 사용해서 함수의 반환 형식을 연역할 수 있다고 생각하기 쉽지만, decltype이 실행되는 위치에서는 아직 함수가 정의되지 않았기 때문에 c
와 i
를 이용할 수 없다.
함수 반환 형식 위치에 decltype
을 사용하는 대신, auto
와 ->
키워드를 사용하는 후행 반환 형식(trailing return type) 구문을 이용하면 함수의 반환 형식을 연역할 수 있다.
1 | template <typename Container, typename Index> |
여기서 auto
는 ->
이후에 함수 반환 형식을 표기하겠다는 의미일 뿐, auto
를 통해서 형식 연역이 일어나거나 하지 않는다.
C++14 부터는 후행 반환 형식의 ->
부분을 생략해도 된다.
1 | template <typename Container, typename Index> |
이 형태에서는 C++11에서와 달리 auto
에서 c[i]
로 부터 형식 연역이 이루어진다. Access
함수의 auto
는 템플릿 형식 연역과 동일한 방식을 사용한다. 중요한건 여기서 참조가 무시된다는 점이다.
1 | std::vector<int> vec; |
Access
의 반환 형식 연역 결과가 l-value
이기 때문에 함수 설계 의도와 다르게 컴파일 에러가 발생한다.
이런 문제를 해결하려면 함수의 반환 형식이 c[i]
의 반환 형식과 동일해야 한다. 명시적으로 후행 반환 형식 구문을 사용한다면 c[i]
의 반환 형식을 수정없이 그대로 얻을 수 있다.
1 | template <typename Container, typename Index> |
C++14에서는 decltype(auto)
지정자를 이용해서 ->
부분을 생략 가능하다.
1 | template <typename Container, typename Index> |
decltype(auto)
지정자는 형식 연역을 의미하는 auto
를 decltype
의 인자로 사용함으로써, '형식 연역을 하되, 그 과정에서 decltype
의 규칙을 적용한다.'라는 뜻을 나타낸다. decltype(auto)
지정자는 함수의 반환타입 뿐만 아니라 일반 변수를 선언할 때에도, 초기화 표현식에 decltype
규칙을 적용하고 싶다면 사용할 수 있다.
1 | Widget w; |
마지막으로 Access
함수의 컨테이너 c
는 non-const 객체에 대한 l-value reference이다. 이는 함수의 반환값을 사용자가 수정할 수 있도록 하기 위해서이다. 하지만 이때문에 함수에 r-value 컨테이너 객체를 전달할 수 없다. 이를 허용하려면 c
를 const 객체에 대한 l-value reference로 선언하면 되지만, 그렇게하면 함수의 반환값을 사용자가 수정할 수 없다.
우리가 원하는 형태는 다음 두 가지를 만족하는 함수이다.
1 | auto s = Access(std::vector<int>{ 1, 2, 3, 4, 5 }, 3); // 임시 벡터의 인덱스 3에 위치한 값을 복사 |
각각의 타입을 받는 버전을 갖는 템플릿 함수를 2개 작성할 수도 있지만 r-value reference를 사용하면 위에서 본 것과 같이 한 번에 처리할 수 있다.
1 | template <typename Container, typename Index> |
마찬가지로 C++14부터는 다음과 같이 간단히 표현할 수 있다.
1 | template <typename Container, typename Index> |
References
C++ Type deduction