형식연역 (Deducing Types)
형식 연역 (template)은 타입에 대한 수정이 소스코드의 전반적 수정으로 전파되는 것을 막아준다.
하지만 동시에, 개발자가 의도한 타입인지 예측하는 것은 어려워졌다.
1. 템플릿 형식 연역 규칙을 숙지해라.
auto 가 연역하는 방식과, C++98 에도 존재했던 템플릿이 연역하는 방식은 일치한다.
따라서 template 연역 규칙을 숙지한다면, auto 연역 규칙에도 동일하게 적용할 수 있다.
template <typename T>
void fn(ParamType a)
fn(expr);
위 예시에서`ParamType` 과 `T` 는 다를 수도 있다.
const, &, && 등 수식어가 ParamType 에 붙을 수 있기 때문이다.
템플릿 형식 연역 규칙은, ParamType 종류에 따라 3가지 케이스로 나뉜다.
- ParamType 이 포인터나 참조이나, 보편 참조가 아니다.
- ParamType 이 보편참조이다.
- ParamType 이 포인터나 참조가 아니다.
1. ParamType 이 포인터나 참조이나, 보편 참조가 아니다
template <typename T>
void fn(T & param)
int i = 10;
const int ci = i;
const int & ri = i;
fn(i); // T = int, param = int &
fn(ci); // T = const int, param = const int &
fn(ri); // T = const int, param = const int &
expr 에 들어오는 변수의 참조성은 무시된다.
2. ParamType 이 보편참조이다.
template <typename T>
void fn(T && param)
int i = 10;
const int ci = i;
const int & ri = i;
fn(i); // T = int &, param = int &
fn(ci); // T = const int &, param = const int &
fn(ri); // T = const int &, param = const int &
fn(10); // T = int, param = int &&
expr 값이 l-value 인지 r-value 인지에 따라 연역 규칙이 상이하다.
- l-value 가 전달되는 경우, l-value reference 형태로 연역된다.
- ex) T = int &, ParamType = int &
- r-value 가 전달되는 경우, r-value reference 형태로 연역된다
- ex) T = int, ParamType = int &&
3. ParamType 이 포인터나 참조가 아니다.
template <typename T>
void fn(T param)
int i = 10;
const int ci = i;
const int &ri = i;
fn(i); // T = int, param = int
fn(ci); // T = int, param = int
fn(ri); // T = int, param = int
값이 복사되는 케이스이므로, expr 에서의 참조, const는 무시된다. (volatile도 무시)
배열, 함수와 같은 특이한 인자의 케이스)
배열의 경우 첫번째 원소는 주소를 나타낸다.
expr 에 배열이 들어가면, template 연역 규칙에 따라 포인터로 변경된다.
함수도 마찬가지로, 함수 포인터로 변경된다.
3. decltype 작동 방식을 숙지하라.
decltype (declared type) 은 객체의 주어진 이름, 표현식 형태 등을 알려준다.
const int i = 5; // decltype(i) = const int
bool f(const Widget & w) // decltype(f) = bool(const Widget &), decltype(w) = const Widget &
또한 형식이 T 이고, 이름이 아닌 l-value 표현식에 대해서는 &T 형식으로 반환한다.
하지만 예상하지 못한 동작을 하는 케이스가 존재한다.
예를 들어, vector, deque 등 컨테이너의 operator[] 반환 형식은 컨테이너마다 다를 수 있다.
따라서 decltype을 사용하여, 이런 반환 형식을 일반화하는 예시를 확인해보자.
#include <iostream>
#include <vector>
template<typename Container, typename Index>
auto ret(Container & c, Index i) -> decltype(c[i]) {
return c[i];
}
int main() {
std::vector<int> v(5);
ret(v, 2) = 5;
std::cout << v[2] << std::endl;
}
함수의 반환 타입은 auto 이나, C++11 에서는 trailing return type 구문 (후행 반환 형식 / -> 뒤에 반환 형식 표현) 을 사용하지 않으면 컴파일 에러가 발생한다.
trailing return type 구문은 기존 return 구문과 다르게, 함수 인자로 들어오는 값을 사용하여 반환 형식을 지정할 수 있는 장점이 있다.
C++ 14 이상부터는 `-> decltype(c[i])` 와 같은 trailing return type 없이 `auto` 와 `return c[i];` 만으로 컴파일러가 반환 형식을 예측하게 할 수 있다.
따라서 auto 는 컴파일러가 '반환 형식을 알아서 예측' 하라는 역할만 하게 된다.
여기서 예상하지 못한 동작이 발생한다.
일단 위 코드에서 `-> decltype(c[i])` 를 제거한다.
컨테이너가 함수 인자로 전달되며, template 연역 과정에서 참조성은 사라지고 r-value 로 반환된다. (1. 템플릿 형식 연역 규칙을 숙지해라 참고)
따라서 `ret(v, 2) = 5;` 는 r-value 를 r-value 에 할당하게 되므로 컴파일 에러가 발생한다.
해결 방법은 `auto` 대신 `decltype(auto)` 를 사용하는 것이다.
- auto : 컴파일러가 '반환 형식을 알아서 예측' 하라
- decltype : 반환 형식을 추론할 때에는 decltype 규칙을 적용해라
이를 사용하면, l-value인 `v[2]` 를 그대로 반환할 수 있다.
함수에 l-value 가 아닌 r-value 를 인자로 넘기고 싶은 경우, `std::forward` 를 활용하고, 인자를 보편 참조 형태 (&&)로 변경하면 된다.
이렇게 코드를 수정하는 경우 l-value, r-value 에 모두 대응 가능하다.
// C++14 이하
template<typename Container, typename Index>
auto ret(Container && c, Index i) -> decltype(std::forward<Container>(c)[i]) {
return std::forward<Container>(c)[i];
// C++14 이상
template<typename Container, typename Index>
decltype(auto) ret(Container && c, Index i) {
return std::forward<Container>(c)[i];
하지만 decltype(auto) 마저도 예상하지 못한 동작을 하는 케이스가 있다.
예를 들어, l-value 를 () 괄호로 감싸는 경우 decltype 이 추론하는 형식이 달라진다.
int x = 0;
decltype(x); // int
decltype((x)); // int &
아래 예시는 () 와 같은 작성 습관에 따라 decltype(auto) 의 반환 추론이 달라져, 내부 변수 참조를 전달하는 문제 있는 코드이다.
#include <iostream>
#include <vector>
decltype(auto) ret() {
int a = 5;
return (a); // & int
}
int main() {
std::cout << ret() << std::endl;
}
'dev > C++' 카테고리의 다른 글
Effective Modern C++ #7 객체 생성 시 괄호()와 중괄호{}를 구분하라. (0) | 2025.06.10 |
---|---|
Effective Modern C++ #13 iterator 보다 const_iterator 를 선호하라 (0) | 2025.06.10 |
Effective Modern C++ #5 명시적 형식 선언보다는 auto 를 선호하라. (0) | 2025.06.08 |
스마트 포인터(smart pointer) 사용 예시 (0) | 2025.05.23 |
스마트 포인터(smart pointer) (0) | 2025.05.23 |