std::remove_cvref 与 std::type_identity¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 2/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-26 |
编译器支持:GCC 9+ · Clang 8+ · MSVC 19.21+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 特性可用时把 remove_cvref 别名到标准实现 | ev-abseil-remove-cvref-alias-001 |
| 特性可用时把 type_identity 别名到标准实现 | ev-abseil-type-identity-alias-002 |
| 用 std::remove_cvref_t 把成员表达式还原成裸 tuple 类型 | ev-drogon-remove-cvref-coroutine-003 |
本站证据页:ev-abseil-remove-cvref-alias-001 · ev-abseil-type-identity-alias-002 · ev-drogon-remove-cvref-coroutine-003
解决什么问题¶
转发引用会把实参类型推导成 U&、const U& 等形式。想把“去掉引用和顶层 cv 后的类型”存下来,过去要写 std::remove_cv_t<std::remove_reference_t<T>>;用 std::decay_t 又会顺带把数组和函数退化为指针,语义过重。
std::type_identity 解决的是另一个问题:模板推导时,某些位置需要“不推导”的上下文。它把类型原样包一层,让编译器停止从实参推导,常用于元编程、检测惯用法和重载消歧。
C++20 把这两个工具收进标准库:std::remove_cvref_t<T> 精确去掉引用与顶层 cv,std::type_identity_t<T> 提供非推导的类型恒等包装。
基础用法¶
#include <iostream>
#include <type_traits>
template <typename T> void show() {
static_assert(std::is_same_v<std::remove_cvref_t<T>, int>);
}
int main() {
show<const int &>();
std::type_identity_t<int> value = 3;
std::cout << value << '\n';
}
std::remove_cvref_t<const int&> 是 int;std::type_identity_t<int> 仍然是 int,但它的作用是让类型处于非推导上下文,而不是改变类型本身。
进阶用法¶
转发函数把实参存进成员时,T&& 推导出的 T 可能带引用;先用 remove_cvref_t 得到裸值类型,再决定成员如何声明,可以避免把引用意外存进对象。
template <typename T> struct Holder {
using value_type = std::remove_cvref_t<T>;
T value;
};
template <typename T> Holder(T &&) -> Holder<T>;
template <typename T>
void select(T value, std::type_identity_t<T> same) {
std::cout << value + same << '\n';
}
Holder 的推导指引保留原始 T,成员类型由调用方决定;value_type 则明确剥掉引用和顶层 cv,供算法或接口使用。select(1, 2) 中 T 从第一个参数推导为 int,第二个参数的 type_identity_t<T> 不参与推导,只做类型匹配;如果实参类型不同,编译失败而不是悄悄推导出另一套类型。
与 decay_t 的区别仍在数组和函数:remove_cvref_t<const int(&)[3]> 保持 int[3],decay_t 会退化成 int*。需要保留数组边界时不能图省事。
完整示例见 examples/cpp20/remove_cvref_type_identity.cpp。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| abseil-cpp | absl/meta/type_traits.h#L235-L240 |
特性可用时把 remove_cvref 别名到标准实现 |
兼容层保留旧名称,让 C++20 编译器使用标准库实现 |
| abseil-cpp | absl/meta/type_traits.h#L255-L257 |
特性可用时把 type_identity 别名到标准实现 |
同一个头文件统一新旧类型工具,调用方不必区分标准版本 |
| Drogon | lib/inc/drogon/utils/coroutine.h#L858-L860 |
用 std::remove_cvref_t 还原成员 tuple 类型 |
decltype(member) 带引用,剥掉后才能用 tuple_element_t 取元素 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。前两条由特性测试宏保护,第三条是协程等待器中的实际类型计算。
陷阱与边界¶
remove_cvref 不做数组和函数退化。 std::remove_cvref_t<const int(&)[3]> 是 int[3],不是 int*;需要退化时用 std::decay_t。两者语义不同,不能互相替代。
只去掉顶层 cv。 const int* 经过 remove_cvref 仍然是 const int*;它不会移除指针指向对象的 const。需要移除更深层的 cv 时,写自定义 trait 或逐层处理。
type_identity 不改变类型。 它的价值在推导语境,而不是类型变换。把 type_identity_t<int> 当成“某种 int 包装”会导致误用。
非推导上下文有明确边界。 type_identity_t<T> 可以阻止从该参数推导 T,但不会阻止其他参数推导;重载设计仍要检查整体推导结果。
不要用 decay_t 图省事。 decay_t 会把数组和函数退化为指针,还会去掉引用与 cv;在只想剥掉引用和顶层 cv 的场景,它会悄悄改变语义。
按值存储时仍要转发。 remove_cvref_t<T> 只是类型计算;把参数存入容器或回调时,仍要用 std::forward 保留调用方的值类别,否则会多一次拷贝。
特性支持要检测。 __cpp_lib_remove_cvref 与 __cpp_lib_type_identity 分别对应两个设施;旧标准库可能只提供其中一个。
库联动¶
- Abseil:
type_traits.h在特性宏下把remove_cvref与type_identity别名到标准实现,旧标准库则保留自定义 trait。 - Drogon:协程等待器用
std::remove_cvref_t处理成员 tuple 类型,再交给std::tuple_element_t计算每个任务的结果类型。 - 与
std::forward_like的分工:C++23 的forward_like在转发成员或嵌套对象时保持值类别;remove_cvref只做类型剥离,不负责转发表达式。
横向对比¶
| 方案 | 去掉引用 | 去掉顶层 cv | 数组/函数退化 | 主要代价 |
|---|---|---|---|---|
std::remove_cvref_t |
是 | 是 | 否 | C++20 起可用;只处理顶层 cv |
std::decay_t |
是 | 是 | 是 | 语义更重,可能把数组退化为指针 |
remove_cv_t<remove_reference_t<T>> |
是 | 是 | 否 | 写法冗长,C++20 前常见的替代 |
std::type_identity_t |
否 | 否 | 否 | 不改变类型,只影响模板推导 |
| 自定义 trait | 按需 | 按需 | 按需 | 需要维护,容易与标准行为产生偏差 |
跨标准演进:C++20 通过 P0550R2 引入 std::remove_cvref,通过 P0887R1 引入 std::type_identity。C++23 的 std::forward_like 继续完善转发场景,但它解决的是“按对象值类别转发成员”的问题,不替代这两个基础类型工具。