C++ decltype | "타입 추출" 가이드

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: 언제 무엇을 쓰나

구분autodecltype / 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++11auto f(T a, U b) -> decltype(a + b)후행 반환으로 decltype에 의존
C++14auto f(T a, U b) { return a + b; }반환형 auto 추론
C++14decltype(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 |