C++ STL 之 type_traits 与 Concepts 详解
三段式:先讲 traits 速查,再讲 SFINAE 与 Concepts 原理,最后面试题。面向面试与泛型编程进阶。
一、核心 type_traits 速查
1.1 类型查询
cpp
#include <type_traits>
using namespace std;
static_assert(is_same_v<int, int>); // true
static_assert(is_same_v<int, const int>); // false(去掉修饰才相等)
struct Base {};
struct Derived : Base {};
static_assert(is_base_of_v<Base, Derived>); // true
static_assert(is_base_of_v<Base, Base>); // true
static_assert(is_integral_v<int>); // true
static_assert(is_integral_v<float>); // false
static_assert(is_integral_v<char>); // true
is_same<T, U>:严格类型相等,不剥修饰is_base_of<B, D>:B 是 D 的基类(或同一类型)为 trueis_integral<T>:整数族(bool/char/int/long 等)
1.2 类型变换
cpp
#include <type_traits>
using namespace std;
using T1 = remove_reference_t<int&>; // int
using T2 = remove_reference_t<int&&>; // int
using T3 = remove_reference_t<int>; // int
using T4 = decay_t<int&>; // int(剥引用 + cv + 数组/函数退指针)
using T5 = decay_t<const int[4]>; // const int*
using T6 = decay_t<void()>; // void(*)()
decay 干了三件事:剥引用 → 剥顶层 cv → 数组/函数退化为指针。等价于按值传参时编译器的类型推导规则。
面试高频 :decay_t<const int&> → int;decay_t<int(&)[5]> → int*。
1.3 编译期分支 ------ conditional
cpp
using T = conditional_t<is_integral_v<T>, int, double>;
// T 为 int 当 T 是整数,否则 double
相当于编译期三目运算符。常用于泛型代码中根据类型选实现。
二、SFINAE ------ 替换失败不是错误
2.1 原则
SFINAE(Substitution Failure Is Not An Error):模板实例化时,如果某个重载的替换产生非法类型,编译器不报错,仅将该重载从候选集移除。
2.2 enable_if
cpp
template<typename T>
enable_if_t<is_integral_v<T>, T> half(T val) {
return val / 2;
}
template<typename T>
enable_if_t<!is_integral_v<T>, T> half(T val) {
return val / 2.0;
}
两个 half 只有一个能被选定。参数类型决定哪个 enable_if 的 ::type 存在。
2.3 void_t
void_t 把任意类型序列映射为 void,结合 decltype 探测成员是否存在:
cpp
template<typename, typename = void>
struct has_size : false_type {};
template<typename T>
struct has_size<T, void_t<decltype(declval<T>().size())>> : true_type {};
如果 T.size() 合法,偏特化命中,has_size<T> 为 true。
#mermaid-svg-Tc8iIwjFSVp5Iyj0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .error-icon{fill:#552222;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .marker.cross{stroke:#333333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 p{margin:0;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster-label text{fill:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster-label span{color:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster-label span p{background-color:transparent;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .label text,#mermaid-svg-Tc8iIwjFSVp5Iyj0 span{fill:#333;color:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node rect,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node circle,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node ellipse,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node polygon,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .rough-node .label text,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node .label text,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .image-shape .label,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .rough-node .label,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node .label,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .image-shape .label,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .icon-shape .label{text-align:center;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node.clickable{cursor:pointer;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .arrowheadPath{fill:#333333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster text{fill:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .cluster span{color:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .icon-shape,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .icon-shape p,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .icon-shape .label rect,#mermaid-svg-Tc8iIwjFSVp5Iyj0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tc8iIwjFSVp5Iyj0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
模板实例化
替换是否产生
非法类型?
此重载从候选集移除
保留为重载候选
继续检查下一重载
重载决议
唯一匹配成功
或编译错误
2.4 C++17 逻辑 traits
cpp
// conjunction:全 true 才 true(短路求值)
static_assert(conjunction_v<is_integral<int>, is_floating_point<double>>);
// disjunction:任一 true 则 true
static_assert(disjunction_v<is_integral<float>, is_same<int, int>>);
// negation:取反
static_assert(negation_v<is_integral<double>>);
conjunction 短路:一旦遇到 false_type,后续不再实例化,避免触发不合法类型。
三、C++20 Concepts
3.1 定义与使用
cpp
template<typename T>
concept Integral = is_integral_v<T>;
template<typename T>
concept SignedIntegral = Integral<T> && is_signed_v<T>;
requires 子句约束模板:
cpp
template<typename T>
requires Integral<T>
T half(T val) { return val / 2; }
等价写法:
cpp
template<Integral T>
T half(T val) { return val / 2; }
auto half(Integral auto val) { return val / 2; } // 缩写
3.2 标准 concept
cpp
template<typename T, typename U>
requires same_as<T, U>
void assign(T& t, const U& u);
template<typename From, typename To>
requires convertible_to<From, To>
To cast(From&& val);
template<typename F, typename... Args>
requires invocable<F, Args...>
auto invoke(F&& f, Args&&... args) -> decltype(auto);
same_as<T, U>:严格相等(含 cv 修饰)convertible_to<From, To>:From 可隐式转换为 Toinvocable<F, Args...>:F(Args...) 可调用
3.3 短路与约束排序
Concepts 的 && 和 || 也是短路求值:
cpp
template<typename T>
requires is_integral_v<T> && (sizeof(T) <= 4)
void process(T val);
template<typename T>
requires is_integral_v<T>
void process(T val);
当 T = short 时,第一个 concept 部分序(partial ordering)更特化,优先匹配。
渲染错误: Mermaid 渲染失败: Parse error on line 3: ... A候选1: Integral \&\& (sizeof ≤ 4) -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
约束偏序规则:如果 concept A 蕴含 concept B(A 的原子约束是 B 的超集),则 A 更特化。conjunction && 追加约束会提升特化度。
原子约束是去重后的不可分解条件 。conjunction_v<A, B> 和 A && B 会合并为同一个原子约束包,不会产生二义。
四、面试题
Q1 :decay_t<const char[6]> 的结果是什么?
const char*。数组退指针,cv 修饰在元素类型上,指针本身无顶层 cv。
Q2 :enable_if 为什么能用于函数重载?原理是什么?
enable_if<cond>::type 只有 cond=true 时才存在。SFINAE 让替换失败的版本不参与重载决议。
Q3 :写出一个 void_t 探测是否有 reserve 成员的 traits。
cpp
template<typename, typename = void>
struct has_reserve : false_type {};
template<typename T>
struct has_reserve<T, void_t<decltype(declval<T>().reserve(0U))>>
: true_type {};
Q4 :C++17 conjunction 和手动 && 的 enable_if 区别在哪?
短路求值。conjunction 遇到 false_type 不再实例化后续模板,避免编译错误;多层嵌套的 enable_if 无法短路。
Q5 :std::same_as<T, U> 通常定义为两个版本,为什么?
cpp
template<class T, class U>
concept same_as = is_same_v<T, U>; // 不完整
标准实现用两个对称的 concept 来避免部分序歧义:
cpp
template<class T, class U>
concept same_as_impl = is_same_v<T, U>;
template<class T, class U>
concept same_as = same_as_impl<T, U> && same_as_impl<U, T>;
对称约束确保两边查全。
Q6:Concepts 和 SFINAE 比,优势在哪?
- 可读性 :
template<Integral T>比enable_if_t<is_integral_v<T>>清晰得多 - 错误信息:Concept 失败直接报 concept 名而非长串模板实例化栈
- 约束排序 :编译器自动按原子约束做偏序,无需
enable_if的层层嵌套 - 短路 :
requires子句中的&&天然短路
Q7(进阶):如何用 concept 判断一个类型是 "range"?
cpp
template<typename T>
concept Range = requires(T& t) {
begin(t);
end(t);
requires input_iterator<decltype(begin(t))>;
};
requires 表达式可以嵌套 requires 子句做二阶条件。
从
is_same到void_t、从enable_if到 Concept,C++ 的类型元编程经历了从"黑魔法"到"声明式"的演进。Concepts 不是替代 SFINAE,而是在它之上提供一个更安全、更清晰、更可组合的层。凡能用 concept 表达的,不问 SFINAE。