C++ Type Traits | <type_traits> Complete Guide
이 글의 핵심
C++ type traits: is_integral, remove_reference, SFINAE with enable_if, void_t, and compile-time branches with if constexpr.
What are type traits?
Type traits are compile-time utilities that query and transform types. They enable metaprogramming and generic code that adapts to different types.
#include <type_traits>
#include <iostream>
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Processing integer: " << value << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Processing float: " << value << "\n";
} else {
std::cout << "Processing other type\n";
}
}
int main() {
process(42); // "Processing integer: 42"
process(3.14); // "Processing float: 3.14"
process("text"); // "Processing other type"
}
Basic type queries
Primary type categories
#include <type_traits>
// Integral types
static_assert(std::is_integral_v<int>);
static_assert(std::is_integral_v<char>);
static_assert(std::is_integral_v<bool>);
static_assert(!std::is_integral_v<float>);
// Floating-point types
static_assert(std::is_floating_point_v<float>);
static_assert(std::is_floating_point_v<double>);
static_assert(!std::is_floating_point_v<int>);
// Pointer types
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_pointer_v<char*>);
static_assert(!std::is_pointer_v<int>);
// Array types
static_assert(std::is_array_v<int[10]>);
static_assert(!std::is_array_v<int*>);
// Reference types
static_assert(std::is_reference_v<int&>);
static_assert(std::is_reference_v<int&&>);
static_assert(!std::is_reference_v<int>);
Composite type categories
// Arithmetic (integral or floating-point)
static_assert(std::is_arithmetic_v<int>);
static_assert(std::is_arithmetic_v<double>);
static_assert(!std::is_arithmetic_v<std::string>);
// Scalar (arithmetic, pointer, enum, nullptr_t)
static_assert(std::is_scalar_v<int>);
static_assert(std::is_scalar_v<int*>);
static_assert(!std::is_scalar_v<std::vector<int>>);
// Object types
static_assert(std::is_object_v<int>);
static_assert(std::is_object_v<std::string>);
static_assert(!std::is_object_v<int&>);
Type transformations
Remove qualifiers
#include <type_traits>
// Remove const
using T1 = std::remove_const_t<const int>; // int
using T2 = std::remove_const_t<const int*>; // const int* (pointer itself not const)
// Remove volatile
using T3 = std::remove_volatile_t<volatile int>; // int
// Remove cv (const and volatile)
using T4 = std::remove_cv_t<const volatile int>; // int
// Remove reference
using T5 = std::remove_reference_t<int&>; // int
using T6 = std::remove_reference_t<int&&>; // int
using T7 = std::remove_reference_t<int>; // int
// Remove pointer
using T8 = std::remove_pointer_t<int*>; // int
using T9 = std::remove_pointer_t<int**>; // int*
Decay
// Decay: array/function to pointer, remove cv and reference
using T1 = std::decay_t<int&>; // int
using T2 = std::decay_t<const int&>; // int
using T3 = std::decay_t<int[10]>; // int*
using T4 = std::decay_t<int(int)>; // int(*)(int)
Add qualifiers
// Add const
using T1 = std::add_const_t<int>; // const int
// Add pointer
using T2 = std::add_pointer_t<int>; // int*
// Add lvalue reference
using T3 = std::add_lvalue_reference_t<int>; // int&
// Add rvalue reference
using T4 = std::add_rvalue_reference_t<int>; // int&&
Type relationships
#include <type_traits>
// Same type
static_assert(std::is_same_v<int, int>);
static_assert(!std::is_same_v<int, long>);
static_assert(!std::is_same_v<int, int&>);
// Convertible
static_assert(std::is_convertible_v<int, double>);
static_assert(std::is_convertible_v<int*, void*>);
static_assert(!std::is_convertible_v<int, std::string>);
// Base of
class Base {};
class Derived : public Base {};
static_assert(std::is_base_of_v<Base, Derived>);
static_assert(!std::is_base_of_v<Derived, Base>);
SFINAE with enable_if
Function overloading
#include <type_traits>
#include <iostream>
// For integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
twice(T value) {
return value * 2;
}
// For floating-point types
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
twice(T value) {
return value * 2.0;
}
int main() {
std::cout << twice(5) << "\n"; // 10 (integral version)
std::cout << twice(2.5) << "\n"; // 5.0 (floating version)
}
Return type SFINAE
template<typename T>
auto getValue(T& container) -> decltype(container[0]) {
return container[0];
}
std::vector<int> vec = {1, 2, 3};
int first = getValue(vec); // OK: vector has operator[]
// std::list<int> lst = {1, 2, 3};
// auto x = getValue(lst); // Error: list doesn't have operator[]
if constexpr branches
#include <type_traits>
#include <iostream>
#include <vector>
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer: " << value << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Float: " << value << "\n";
} else if constexpr (std::is_pointer_v<T>) {
std::cout << "Pointer: " << value << "\n";
} else {
std::cout << "Other type\n";
}
}
int main() {
process(42); // "Integer: 42"
process(3.14); // "Float: 3.14"
int x = 10;
process(&x); // "Pointer: 0x..."
process("hello"); // "Other type"
}
Custom traits with void_t
Detect member types
#include <type_traits>
// Primary template
template<typename T, typename = void>
struct has_value_type : std::false_type {};
// Specialization for types with value_type
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>>
: std::true_type {};
template<typename T>
inline constexpr bool has_value_type_v = has_value_type<T>::value;
// Usage
static_assert(has_value_type_v<std::vector<int>>);
static_assert(!has_value_type_v<int>);
Detect member functions
#include <type_traits>
#include <utility>
// Detect if type has size() method
template<typename T, typename = void>
struct has_size : std::false_type {};
template<typename T>
struct has_size<T, std::void_t<
decltype(std::declval<T>().size())
>> : std::true_type {};
template<typename T>
inline constexpr bool has_size_v = has_size<T>::value;
// Usage
static_assert(has_size_v<std::vector<int>>);
static_assert(has_size_v<std::string>);
static_assert(!has_size_v<int>);
Detect container
template<typename T, typename = void>
struct is_container : std::false_type {};
template<typename T>
struct is_container<T, std::void_t<
typename T::value_type,
typename T::iterator,
decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end()),
decltype(std::declval<T>().size())
>> : std::true_type {};
template<typename T>
inline constexpr bool is_container_v = is_container<T>::value;
// Usage
static_assert(is_container_v<std::vector<int>>);
static_assert(is_container_v<std::list<double>>);
static_assert(!is_container_v<int>);
Real-world examples
1. Generic serialization
#include <type_traits>
#include <string>
#include <sstream>
template<typename T>
std::string serialize(const T& value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return "\"" + value + "\"";
} else if constexpr (std::is_pointer_v<T>) {
std::ostringstream oss;
oss << static_cast<const void*>(value);
return oss.str();
} else {
return "<unknown>";
}
}
// Usage
auto s1 = serialize(42); // "42"
auto s2 = serialize(3.14); // "3.140000"
auto s3 = serialize(std::string("hello")); // "\"hello\""
2. Optimized swap
#include <type_traits>
#include <utility>
template<typename T>
void optimized_swap(T& a, T& b) {
if constexpr (std::is_trivially_copyable_v<T> && sizeof(T) <= 64) {
// Fast path for small trivial types
T temp = a;
a = b;
b = temp;
} else {
// Use move semantics for larger types
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
}
3. Type-safe printf
#include <type_traits>
#include <iostream>
template<typename T>
void print_value(const T& value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "%d: " << value;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "%f: " << value;
} else if constexpr (std::is_pointer_v<T>) {
std::cout << "%p: " << value;
} else {
std::cout << value;
}
}
template<typename....Args>
void safe_printf(Args....args) {
(print_value(args), ...);
std::cout << "\n";
}
// Usage
safe_printf(42, 3.14, "hello");
Performance implications
Zero runtime cost: All type traits are evaluated at compile time.
// Both produce identical assembly
template<typename T>
T add(T a, T b) {
if constexpr (std::is_integral_v<T>) {
return a + b; // Integer addition
} else {
return a + b; // Floating-point addition
}
}
int x = add(5, 3); // Compiles to: add eax, ebx
double y = add(2.5, 1.5); // Compiles to: addsd xmm0, xmm1
Type traits vs C++20 Concepts
Type traits (C++11+)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
square(T x) {
return x * x;
}
Concepts (C++20+)
template<std::integral T>
T square(T x) {
return x * x;
}
Concepts advantages:
- More readable
- Better error messages
- Can be composed Type traits advantages:
- Works in C++11/14/17
- More flexible for complex conditions
Compiler support
| Compiler | <type_traits> | _v helpers | if constexpr |
|---|---|---|---|
| GCC | 4.3+ | 7+ (C++17) | 7+ (C++17) |
| Clang | 3.0+ | 3.9+ (C++17) | 3.9+ (C++17) |
| MSVC | 2010+ | 2015+ (C++17) | 2017+ (C++17) |
Related posts
Keywords
C++, type traits, SFINAE, templates, metaprogramming, compile-time, enable_if, if constexpr, void_t
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ type traits: is_integral, remove_reference, SFINAE with enable_if, void_t, and compile-time branches with `i… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ SFINAE | ‘Substitution Failure Is Not An Error’ 가이드
- C++ enable_if | ‘조건부 컴파일’ 가이드
- C++ Template Lambda | ‘템플릿 람다’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, type-traits, templates, SFINAE, metaprogramming 등으로 검색하시면 이 글이 도움이 됩니다.