C++ decltype | "타입 추출" 가이드
이 글의 핵심
decltype은 표현식의 타입을 그대로 가져옵니다. auto와의 차이, decltype(auto), 반환 타입 추론, SFINAE 패턴까지 한 글에서 정리합니다.
decltype이란?
decltype 은 표현식의 타입을 추출하는 C++11 키워드입니다. auto와 달리 const와 참조를 유지합니다.
int x = 10;
decltype(x) y = 20; // int y = 20;
const int& ref = x;
decltype(ref) z = x; // const int& z = x;
왜 필요한가?:
- 정확한 타입: const, 참조 유지
- 템플릿: 반환 타입 추론
- 타입 안전: 컴파일 타임 타입 체크
- 제네릭 코드: 타입 독립적 코드
// ❌ auto: const, 참조 제거
const int& ref = x;
auto a = ref; // int (const, 참조 제거)
// ✅ decltype: 정확한 타입
decltype(ref) b = ref; // const int& (그대로 유지)
decltype의 동작 원리:
decltype은 컴파일 타임에 타입을 추론합니다. 표현식을 평가하지 않고, 타입만 추출합니다.
int func() {
std::cout << "호출됨\n";
return 42;
}
decltype(func()) x; // int x; (func() 호출 안됨!)
decltype 규칙:
| 표현식 | 타입 | 예시 |
|---|---|---|
| 변수명 | 선언된 타입 | decltype(x) → int |
| 표현식 (괄호) | 값 카테고리에 따라 | decltype((x)) → int& |
| 함수 호출 | 반환 타입 | decltype(func()) → int |
| 연산자 | 결과 타입 | decltype(x + y) → double |
int x = 10;
// 변수명: 선언된 타입
decltype(x) a; // int
// 표현식 (괄호): lvalue → 참조
decltype((x)) b = x; // int&
// 함수 호출: 반환 타입
int func();
decltype(func()) c; // int
// 연산자: 결과 타입
decltype(x + 1.0) d; // double
auto vs decltype
int x = 10;
const int& ref = x;
auto a = ref; // int (const, 참조 제거)
decltype(ref) b = ref; // const int& (그대로 유지)
기본 사용법
int x = 10;
double y = 3.14;
// 변수 타입 추출
decltype(x) a = 20; // int
decltype(y) b = 2.71; // double
decltype(x + y) c = 0; // double
// 함수 반환 타입
int func() { return 42; }
decltype(func()) result = func(); // int
반환 타입 추론
// C++11: 후행 반환 타입
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// C++14: 자동 추론
template<typename T, typename U>
auto multiply(T a, U b) {
return a * b;
}
int main() {
auto result1 = add(10, 3.14); // double
auto result2 = multiply(5, 2.5); // double
}
실전 예시
예시 1: 템플릿 함수
template<typename Container>
auto getFirst(Container& c) -> decltype(c[0]) {
return c[0];
}
int main() {
vector<int> vec = {1, 2, 3};
auto& first = getFirst(vec); // int&
first = 10;
cout << vec[0] << endl; // 10
}
예시 2: 완벽한 전달
template<typename T>
auto forward_value(T&& value) -> decltype(std::forward<T>(value)) {
return std::forward<T>(value);
}
void process(int& x) {
cout << "lvalue: " << x << endl;
}
void process(int&& x) {
cout << "rvalue: " << x << endl;
}
int main() {
int x = 10;
process(forward_value(x)); // lvalue
process(forward_value(20)); // rvalue
}
예시 3: 타입 안전 매크로
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 문제: 타입 불일치
auto result1 = MAX(10, 3.14); // double
// decltype으로 해결
template<typename T, typename U>
auto max_value(T a, U b) -> decltype(a > b ? a : b) {
return a > b ? a : b;
}
auto result2 = max_value(10, 3.14); // double
예시 4: 컨테이너 래퍼
template<typename Container>
class Wrapper {
private:
Container& container;
public:
explicit Wrapper(Container& c) : container(c) {}
auto operator[](std::size_t index) -> decltype(container[index]) {
return container[index];
}
auto size() const -> decltype(container.size()) {
return container.size();
}
};
int main() {
std::vector<int> vec = {1, 2, 3};
Wrapper<std::vector<int>> wrapper(vec);
wrapper[0] = 10;
std::cout << vec[0] << '\n';
}
decltype(auto) (C++14)
// decltype(auto): 초기화/반환 표현식에 대해 decltype 규칙으로 타입을 맞춤
int x = 10;
int& getRef() { return x; }
auto a = getRef(); // int (참조 제거)
decltype(auto) b = getRef(); // int& (참조 유지)
// 함수 반환: return 표현식의 정확한 타입을 유지
template<typename T>
decltype(auto) forward_return(T&& value) {
return std::forward<T>(value);
}
주의: decltype(auto) x = expr;에서 expr가 임시(prvalue)이면 참조가 붙지 않습니다. decltype(auto)는 “자동으로 참조를 붙인다”가 아니라 “decltype 규칙을 auto로 적용한다”에 가깝습니다.
decltype vs auto: 언제 무엇을 쓰나
| 구분 | auto | decltype / decltype(auto) |
|---|---|---|
| 초기화 우변에서 | 값 복사 위주(참조·const 종종 제거) | 표현식의 정확한 타입 |
| 변수 선언 | auto x = expr; | decltype(expr) x = expr; 등 |
| 반환 타입 | C++14부터 auto 함수 반환 추론 | 후행 반환 -> decltype(...) 또는 decltype(auto) |
| 용도 | 간결함, 구현 캡슐화 | 참조·const 유지, SFINAE, 타입 추출 |
실무 규칙(대략): “그냥 지역 변수는 auto”, “원본과 참조·const 관계를 유지해야 하면 decltype 또는 decltype(auto)”입니다.
반환 타입 추론의 진화 (C++11 → C++14)
| 표준 | 예시 | 비고 |
|---|---|---|
| C++11 | auto f(T a, U b) -> decltype(a + b) | 후행 반환으로 decltype에 의존 |
| C++14 | auto f(T a, U b) { return a + b; } | 반환형 auto 추론 |
| C++14 | decltype(auto) f(T&& x) { return x; } | 반환 표현식에 decltype 규칙 적용 |
decltype만으로는 함수 본문이 없을 때 반환 타입을 말해 줄 수 있어, C++11 템플릿에서 특히 유용했습니다. C++14 이후에는 decltype(auto)로 “반환을 그대로”를 더 짧게 쓸 수 있습니다.
SFINAE와 decltype
decltype은 표현식이 유효한지를 컴파일 타임에 검사하는 데 쓰일 수 있어, 오버로드 집합에서 후보를 걸러내는 SFINAE와 잘 맞습니다.
후행 반환 + 쉼표 연산자
decltype 안에서 쉼표 연산자를 쓰면, 왼쪽 표현식이 형식적으로 유효해야 전체 decltype이 성공합니다. 그래서 “size()가 있는 타입만” 같은 조건을 오버로드 후보를 걸러내는 데 씁니다.
template<typename T>
auto process(T value) -> decltype(value.size(), void()) {
// value.size() 가 있을 때만 이 오버로드가 선택됨
}
(실제 코드에서는 반환형을 void로 두고 본문을 채우면 됩니다.)
C++17 이후에는 std::void_t<decltype(std::declval<T>().size())> 같은 void_t 특수화로 같은 조건을 더 구조적으로 나누기도 합니다.
std::declval과 조합
객체가 없어도 가상의 값으로 표현식 타입을 검사합니다.
template<typename T, typename U>
using SumResult = decltype(std::declval<T>() + std::declval<U>());
SumResult<int, double>는 double이 됩니다. 컴파일 실패 시 해당 타입 조합에 operator+가 없다는 뜻입니다.
Concepts 이후
C++20 Concepts가 있으면 같은 조건을 더 읽기 쉽게 쓸 수 있지만, 기존 코드베이스와 라이브러리에서는 decltype 기반 SFINAE가 여전히 흔합니다.
decltype 규칙
int x = 10;
// 1. 변수명: 선언된 타입
decltype(x) a; // int
// 2. 표현식: 값 카테고리에 따라
decltype((x)) b = x; // int& (lvalue 표현식)
decltype(x + 1) c; // int (prvalue)
// 3. 함수 호출: 반환 타입
int func();
decltype(func()) d; // int
자주 발생하는 문제
문제 1: 괄호 주의
int x = 10;
// ❌ 괄호 하나 더
decltype((x)) y = x; // int& (참조!)
y = 20;
cout << x << endl; // 20
// ✅ 괄호 없이
decltype(x) z = x; // int (복사)
z = 30;
cout << x << endl; // 20
문제 2: 초기화되지 않은 변수
// ❌ 초기화 필요
decltype(10) x; // int x; (초기화 안됨)
cout << x << endl; // 쓰레기 값
// ✅ 초기화
decltype(10) y = 0;
문제 3: 복잡한 표현식
int x = 10;
int* ptr = &x;
// 복잡한 타입
decltype(*ptr) a = x; // int& (역참조는 lvalue)
decltype(ptr[0]) b = x; // int& (배열 접근은 lvalue)
decltype vs auto
int x = 10;
const int& ref = x;
// auto: const, 참조 제거
auto a = ref; // int
// decltype: 정확한 타입
decltype(ref) b = ref; // const int&
// decltype(auto): 표현식의 정확한 타입
decltype(auto) c = ref; // const int&
실용 예시
예시 1: 제네릭 람다
// C++14: 제네릭 람다는 보통 auto 매개변수로 충분
auto lambda = [](auto x, auto y) { return x + y; };
std::cout << lambda(10, 3.14) << '\n';
// 후행 반환을 명시할 때는 decltype으로 연산 결과 타입을 고정
auto lambda2 = [](auto x, auto y) -> decltype(x + y) { return x + y; };
예시 2: SFINAE
#include <type_traits>
template<typename T>
auto process(T value) -> decltype(value.size(), void()) {
cout << "컨테이너: " << value.size() << endl;
}
template<typename T>
auto process(T value) -> decltype(value + 0, void()) {
cout << "숫자: " << value << endl;
}
int main() {
process(vector<int>{1, 2, 3}); // 컨테이너: 3
process(42); // 숫자: 42
}
예시 3: 타입 추출
template<typename T>
class TypeInfo {
public:
using value_type = T;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
// decltype으로 멤버 타입 추출
template<typename U>
using result_type = decltype(std::declval<T>() + std::declval<U>());
};
int main() {
TypeInfo<int>::value_type x = 10;
TypeInfo<int>::reference ref = x;
TypeInfo<int>::result_type<double> result = 3.14;
}
실무 패턴
패턴 1: 제네릭 래퍼
template<typename Container>
class ContainerWrapper {
Container& container_;
public:
explicit ContainerWrapper(Container& c) : container_(c) {}
auto operator[](std::size_t index) -> decltype(container_[index]) {
return container_[index];
}
auto size() const -> decltype(container_.size()) {
return container_.size();
}
};
// 사용
std::vector<int> vec = {1, 2, 3};
ContainerWrapper<std::vector<int>> wrapper(vec);
wrapper[0] = 10; // 참조로 반환되어 수정 가능
std::cout << vec[0] << '\n'; // 10
패턴 2: 타입 안전 팩토리
template<typename T, typename... Args>
auto makeObject(Args&&... args) -> decltype(T(std::forward<Args>(args)...)) {
return T(std::forward<Args>(args)...);
}
// 사용
auto obj1 = makeObject<std::string>("Hello");
auto obj2 = makeObject<std::vector<int>>(10, 42);
패턴 3: 조건부 반환 타입
template<typename T>
auto getValue(T& container, size_t index)
-> decltype(container[index])
{
if (index < container.size()) {
return container[index];
}
throw std::out_of_range("Index out of range");
}
// 사용
std::vector<int> vec = {1, 2, 3};
auto& value = getValue(vec, 0); // int&
value = 10;
FAQ
Q1: decltype은 언제 사용하나요?
A:
- 정확한 타입 필요: const, 참조 유지
- 템플릿 반환 타입: 타입 독립적 코드
- 참조 유지: 원본 수정 가능
template<typename T>
auto getFirst(T& container) -> decltype(container[0]) {
return container[0];
}
Q2: auto vs decltype?
A:
auto: 간결, const/참조 제거decltype: 정확한 타입 유지
const int& ref = x;
auto a = ref; // int (const, 참조 제거)
decltype(ref) b = ref; // const int& (그대로 유지)
Q3: decltype(auto)는 무엇인가요?
A: C++14에서 도입된 기능으로, 표현식의 정확한 타입을 자동 추론합니다.
int& getRef();
auto a = getRef(); // int (참조 제거)
decltype(auto) b = getRef(); // int& (참조 유지)
Q4: 괄호의 의미는?
A:
decltype(x): 변수 타입decltype((x)): 표현식 타입 (참조)
int x = 10;
decltype(x) a; // int
decltype((x)) b = x; // int& (참조!)
Q5: 성능은?
A: 컴파일 타임에 타입을 추론하므로 런타임 오버헤드가 없습니다.
decltype(func()) x; // func() 호출 안됨!
Q6: decltype은 표현식을 평가하나요?
A: 아니요. decltype은 타입만 추출하고, 표현식을 평가하지 않습니다.
int func() {
std::cout << "호출됨\n";
return 42;
}
decltype(func()) x; // "호출됨" 출력 안됨
Q7: 템플릿에서 decltype 사용법은?
A: 후행 반환 타입으로 사용합니다.
// C++11: 후행 반환 타입
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// C++14: decltype(auto)
template<typename T, typename U>
decltype(auto) add(T a, U b) {
return a + b;
}
Q8: decltype 학습 리소스는?
A:
- “Effective Modern C++” by Scott Meyers (Item 3)
- cppreference.com - decltype
- “C++ Templates: The Complete Guide” by Vandevoorde & Josuttis
관련 글: auto, decltype(auto), trailing-return-type.
한 줄 요약: decltype은 표현식의 타입을 추출하는 C++11 키워드로, const와 참조를 유지합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ auto 키워드 | “타입 추론” 가이드
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
관련 글
- C++ auto 키워드 |
- C++ auto 타입 추론 에러 |
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
- C++ async & launch |
- C++ Atomic Operations |