一、std::move

实现

template<class T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

一行 static_cast,仅此而已。它不移动任何东西,只负责把表达式的值类别改成右值,真正干活的是随后被重载决议选中的移动构造函数。

三个设计点

① 参数为什么是 T&&(转发引用)

因为 std::move 必须能接收左值和右值两种实参。传左值时 T 推成 U&,传右值时 T 推成 U

② 返回类型为什么要 remove_reference_t

这是整个实现的关键。假设去掉它,写成 T&& move(T&& t)

std::string s;
std::move(s);   // T 推导为 string&
                // 返回类型 T&& = string& && —— 引用折叠 —— = string&
                // 返回左值引用 → 表达式是【左值】→ std::move 完全失效!

加上 remove_reference_t<T> 就先把引用剥掉:

实参 T 推导为 remove_reference_t<T> 返回类型
左值 s string& string string&&
右值 string{} string string string&&

两种情况都返回 string&&。而返回右值引用的函数调用是 xvalue,属于右值 —— 目的达成。

noexcept

std::move 本身绝不抛异常,标注它才不会污染调用者的 noexcept 推导(尤其是移动构造函数的 noexcept 判断)。

一个经典坑:const 对象 move 不动

const std::string s = "hi";
std::string t = std::move(s);   // 编译通过,但执行的是【拷贝】

T 推成 const string&remove_reference_t 只去引用不去 const,返回 const string&&。而 string(string&&) 无法绑定 const string&&,重载决议只好退回 string(const string&)

静默降级、没有任何警告。所以:需要被移动的对象不要声明成 const,尤其是类成员和返回值。


二、std::forward

实现(两个重载)

// ① 接收左值
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
    return static_cast<T&&>(t);
}

// ② 接收右值
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
    static_assert(!std::is_lvalue_reference_v<T>,
                  "cannot forward an rvalue as an lvalue");
    return static_cast<T&&>(t);
}

三个设计点

① 为什么必须显式写 std::forward<T>(x)

参数类型是 remove_reference_t<T>&T 出现在 remove_reference_t<T>::type 后面 —— 这是非推导语境(non-deduced context)。编译器无法从实参反推 T,你必须手动提供。

这不是设计缺陷,而是刻意为之:x 作为具名变量永远是左值,从它身上推不出原始调用者传的是左值还是右值。那个信息只保存在外层模板的 T 里,所以必须显式把 T 传进来。

② 核心机制:引用折叠

template<class T>
void wrapper(T&& x) {
    target(std::forward<T>(x));
}
外层调用 T 推导为 static_cast<T&&> 展开 折叠后 结果值类别
wrapper(lval) U& static_cast<U& &&> U& 左值
wrapper(U{}) U static_cast<U&&> U&& 右值(xvalue)

引用折叠规则一句话:只要有一个 &,结果就是 && && &&&& &&;只有 && &&&&)。

std::forward 就是把这条规则当开关用:T 里是否带 &,决定了 cast 的目标是左值引用还是右值引用。

③ 第二个重载是干什么的

用来转发本身就是右值的表达式,而不是具名变量:

std::forward<T>(get_something());   // 实参是右值,走重载②

里面的 static_assert 拦截 std::forward<U&>(某个右值) 这种写法 —— 那等于把一个临时对象当左值转发出去,调用结束后必然悬垂。

std::move 的分工

std::move(x)         // 无条件转右值。用在"我确定不再需要 x"的地方
std::forward<T>(x)   // 有条件:T 带 & 就还原成左值,否则转右值
                     // 只用在转发引用(模板 T&& / auto&&)的转发位置

在转发引用上用 std::move 是 bug

template<class T>
void wrapper(T&& x) {
    target(std::move(x));   // ✗ 调用者传的左值也会被偷走
}

调用者写 wrapper(myVec) 后会发现 myVec 莫名其妙空了。


三、完整示例

template<class... Args>
std::unique_ptr<Widget> make(Args&&... args) {
    return std::unique_ptr<Widget>(new Widget(std::forward<Args>(args)...));
}

std::string name = "abc";
make(name, std::string("tmp"), 42);
// Args 推导为 {std::string&, std::string, int}
// forward 后分别以 左值 / 右值 / 右值 传给 Widget 构造函数
// → name 被拷贝(正确,调用者还要用),临时 string 被移动,42 直接传

这就是完美转发:转发过程既不改变值类别,也不改变 const 性。make_uniqueemplace_backstd::bind 的内部全靠这套机制。


四、面试速答

std::move 是一个无条件的 static_cast<remove_reference_t<T>&&>,返回类型必须先 remove_reference 再加 &&,否则传左值时引用折叠会让它退化成左值引用而失效。 std::forward 是有条件的 static_cast<T&&>,靠引用折叠:T 推导带 & 就折叠成左值引用,不带就是右值引用,从而还原调用者传入的原始值类别。它的参数写成 remove_reference_t<T>& 制造非推导语境,强制显式指定 T —— 因为具名变量本身永远是左值,值类别信息只存在于外层的 T 中。 两者都是纯类型转换,编译后不产生任何指令。

不是编译器黑魔法,是纯粹的库代码,用模板偏特化实现的模式匹配。你自己十行就能写一个。

实现

template<class T> struct remove_reference      { using type = T; };  // 主模板
template<class T> struct remove_reference<T&>  { using type = T; };  // 偏特化①
template<class T> struct remove_reference<T&&> { using type = T; };  // 偏特化②

template<class T>
using remove_reference_t = typename remove_reference<T>::type;

原理:模板偏特化 = 类型层面的模式匹配

编译器在实例化 remove_reference<X> 时,会拿 X 去和所有特化的模式做匹配,选最特殊的那个:

写法 匹配到 推导出的 T ::type
remove_reference<int> 主模板 int int
remove_reference<int&> 偏特化① T& int int
remove_reference<int&&> 偏特化② T&& int int
remove_reference<const int&> 偏特化① T& const int const int

注意最后一行:remove_reference 只剥引用,不剥 const。这正是前面说的"std::move 对 const 对象失效"的原因 —— const string& 剥完是 const string,加上 && 得到 const string&&,绑不上移动构造函数。

想连 cv 一起剥,C++20 提供了 std::remove_cvref_t

template<class T>
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;

顺序不能反 —— 引用没有顶层 cv,必须先去引用。

为什么有 _t 后缀

C++11 只有 remove_reference<T>::type,用起来要写一堆 typename

typename std::remove_reference<T>::type&& move(T&& t);   // C++11
std::remove_reference_t<T>&& move(T&& t);                // C++14 起

typename 是必需的,因为 remove_reference<T>::type依赖名(dependent name),编译器在实例化前不知道 type 是类型还是静态成员,默认按非类型解析,必须用 typename 显式声明。C++14 加的 _t 别名模板把这个噪音消掉了(别名模板自身就声明为类型,不需要 typename)。

同理还有 _v 后缀(C++17),把 is_same<A,B>::value 简化成 is_same_v<A,B>

哪些 type_traits 才是真的编译器内建

<type_traits> 里绝大多数是纯库实现(remove_referenceadd_pointeris_sameconditionalis_pointer 等),但有一批确实做不到,必须靠编译器内建(intrinsic):

trait 为什么必须内建
is_enum / is_union / is_class 语言层面无法区分 class 和 union
is_polymorphic 需要知道有没有虚函数
is_trivially_copyable
is_trivially_destructible
需要编译器对特殊成员函数的内部判定
is_base_of 需要访问不可访问/歧义的基类关系
underlying_type 枚举底层类型只有编译器知道
is_constructible / is_convertible 需要模拟一次完整的重载决议
is_final / is_abstract 语言属性

它们在 libstdc++/libc++ 里长这样:

template<class T>
struct is_enum : bool_constant<__is_enum(T)> {};   // __is_enum 是编译器内建

判断依据很简单:如果一个性质无法通过"构造一个表达式看它能否编译"来侦测,就只能内建。 remove_reference 显然不属于这类。

一个现实中的细节

有意思的是,近几年 Clang 16+ / GCC 也给 remove_reference 加了内建版本(__remove_reference_t),libc++ 会优先用它:

#if __has_builtin(__remove_reference_t)
template<class T> using remove_reference_t = __remove_reference_t<T>;
#else
// 上面那套偏特化
#endif

但原因不是做不到,而是编译速度:模板实例化要建 AST 节点、进符号表、参与 memoization,而 remove_reference 在标准库里被实例化的次数是天文数字。内建版本直接在编译器内部做一次类型变换,零实例化开销。大型项目上这类替换能省下可观的编译时间。

顺带:自己动手实现几个

面试常让手写,这几个都是同样的偏特化套路:

// 去指针
template<class T> struct remove_pointer          { using type = T; };
template<class T> struct remove_pointer<T*>      { using type = T; };
template<class T> struct remove_pointer<T* const>{ using type = T; };
// (还要 T* volatile 和 T* const volatile 两个特化)

// 判断是否同类型
template<class A, class B> struct is_same       : std::false_type {};
template<class A>          struct is_same<A, A> : std::true_type  {};

// 判断是否左值引用
template<class T> struct is_lvalue_reference     : std::false_type {};
template<class T> struct is_lvalue_reference<T&> : std::true_type  {};

is_same 那两行特别能说明问题:偏特化就是在类型上做模式匹配is_same<A,A> 这个模式只有两个实参相同时才匹配得上。整个 <type_traits> 的非内建部分,本质上都是这一个技巧的变奏。

一句话:把类型上的引用剥掉,得到"裸类型"。 T&TT&&TTT

它本身不做任何运行时的事,只是在编译期把类型归一化。归一化之所以必要,是因为模板推导和 decltype 经常给你带引用的类型,而你需要的往往是不带引用的那个。

具体有三类用途。

一、在 std::move 里:让 && 落到实处

这是最经典的一处。目标是"无论传进来什么,都返回右值引用",而 T 推导出来可能带引用:

template<class T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

对比一下有没有它:

实参 T T&& 的返回类型 remove_reference_t<T>&&
左值 s string& string& &&string& string&&
右值 string string&& string&&

没有它,传左值时引用折叠会把返回类型压成 string&std::move 对左值完全失效 —— 而左值恰恰是它唯一的使用场景。先剥干净再加 &&,才能强制得到右值引用。

二、在 std::forward 里:制造非推导语境

template<class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept;

这里用它不是为了剥引用,而是为了T 变得不可推导T 藏在 remove_reference_t<T>::type 后面,编译器无法从实参反推,于是强制你写 std::forward<T>(x)

这是刻意的:x 作为具名变量永远是左值,值类别信息只存在于外层的 T 里,必须手动传进来。

三、在泛型代码里:从"引用类型"拿到"值类型"

decltype 和转发引用给你的类型经常带引用,而你要声明变量、做类型判断时需要裸类型。

声明一个真正的副本

template<class T>
void f(T&& x) {
    std::remove_reference_t<T> copy = x;   // T 可能是 U&,直接写 T copy 会变成引用
}

从迭代器/容器取元素类型

using V = std::remove_reference_t<decltype(*it)>;   // *it 是左值,decltype 给 U&

避开 decltype((x)) 的陷阱

int x = 0;
decltype(x)   a;   // int
decltype((x)) b;   // int& —— 多一层括号就变成了表达式,而 x 是左值

类型比较前先归一化(实践中最常踩坑的一处)

class Widget {
public:
    template<class T>
    Widget(T&& x);              // 万能构造函数
    Widget(const Widget&);
};

Widget w1;
Widget w2(w1);   // ✗ 调的是模板!因为 w1 是非 const 左值,
                 //   模板实例化出 Widget(Widget&) 比 Widget(const Widget&) 更匹配

修法就是用归一化后的类型把自己排除掉:

template<class T,
         class = std::enable_if_t<
             !std::is_same_v<std::remove_cvref_t<T>, Widget>>>
Widget(T&& x);

这里必须用 remove_cvref_t(= remove_cv_t<remove_reference_t<T>>),因为 T 可能是 Widget&const Widget&Widget 中的任何一个,不归一化就比不出相等。C++20 起也可以写成约束:

template<class T>
    requires (!std::same_as<std::remove_cvref_t<T>, Widget>)
Widget(T&& x);

记住一点

只剥引用,不剥 const

std::remove_reference_t<const int&>   // → const int,不是 int

所以做类型比较时,几乎总是应该用 remove_cvref_t 而不是 remove_reference_t。只有在 std::move/std::forward 那种"我要保留 const、只想改引用形态"的场合,才单独用 remove_reference_t

根本原因:C++ 用 & 这一个符号,承载了两件完全不同的事

第一件:程序员声明一个别名 —— int& r = x;,"引用"是我要的类型。

第二件:编译器在模板推导decltype 里,把表达式的值类别偷偷编码成了引用。

template<class T> void f(T&& x);

f(lval);        // T = U&    ← 这个 & 不是"我要引用",是"我传的是左值"
f(U{});         // T = U     ← 没有 & 表示"我传的是右值"

int x;
decltype(x)   // int    ← x 作为实体的类型
decltype((x)) // int&   ← 这个 & 也不是引用,是"这个表达式是左值"

这就是问题所在:T 里同时塞了"是什么类型"和"是什么值类别"两份信息,挤在同一个符号上。 一旦你只想要前者,就必须有个操作把后者剥掉 —— 这就是 remove_reference 的存在意义。

它不是什么类型工具箱里的琐碎零件,而是解码器:把被编码进类型的值类别信息拆出去。

为什么不能"就带着引用用"

因为引用类型在 C++ 类型系统里是个残缺的公民,它无法参与组合:

组合 结果
T& & 不存在"引用的引用",发生引用折叠
T&* 不存在"指向引用的指针"
T&[10] 不存在"引用的数组"
const (T&) 顶层 const 被静默忽略,引用本来就不能重新绑定
sizeof(T&) 等于 sizeof(T),引用自身没有独立的大小

所以任何"拿到 T 之后再往上加东西"的泛型代码,只要 T 可能带引用,构造就会走样。这不是加个 if 能绕过去的,是类型系统层面的:你必须先归一化,才能安全地做类型构造。

std::move 就是最小的例子。它想说的是"给我一个指向同一对象的右值引用",写出来是 T&&。但如果 T 已经是 U&T&& 就不是"T 的右值引用",而是折叠成 U&

// 你的意图:           对 T 加 &&
// 实际发生:           对 (可能已带 &) 的 T 做折叠
// 修复:先归一化,再加  remove_reference_t<T> + &&

归一化是类型构造的前提。 这就是为什么它总是成对出现:remove_reference_t<T>&&remove_reference_t<T>*remove_cvref_t<T> 作为容器的 value_type

再看得深一点:它其实是在做"投影"

用一个坐标来理解:模板推导给你的 T 是一个二元组

T  ≈  (裸类型, 值类别)
      ^^^^^^^  ^^^^^^^
      你要的    编译器塞的

标准库为这两个分量各提供了一套工具:

  • 裸类型分量 → remove_reference_t / remove_cvref_t
  • 值类别分量 → std::forward<T>(它读的正是 T 里有没有 &
  • 覆盖值类别分量 → std::move(先剥掉,再无条件写成 &&

std::movestd::forward 的差别,在这个视角下一目了然:

move(t)       = static_cast<remove_reference_t<T>&&>(t)   // 丢弃原值类别,写死为右值
forward<T>(t) = static_cast<T&&>(t)                       // 保留原值类别,靠折叠还原

一个剥完重写,一个原样保留。 这就是"无条件转换"和"条件转换"的全部含义。

一句话回答

因为 C++ 在模板推导和 decltype 中,把表达式的值类别编码成了类型上的引用,导致 T 里混着两份信息。而引用类型又无法参与进一步的类型构造(不能是引用的引用/指针/数组)。所以当你想基于 T 构造新类型时,必须先用 remove_reference_t 把编码进去的值类别剥掉,拿到干净的裸类型,再做想做的构造 —— 否则引用折叠会让构造出来的东西跟你的意图不符。