前言
std::ref与std::cref定义在<functional>中,C++11 起成为标准。它们的作用一句话就能说完:把某个对象包装成一个「可拷贝的引用」。但这个简单描述背后有两个容易混淆的点。
第一,std::ref不创建引用类型。C++ 没有「引用的引用」这种类型,函数模板按值传参时引用会被丢弃。std::ref返回的是std::reference_wrapper<T>——一个类模板的对象,内部存指针,对外提供到T&的隐式转换。它本身是值类型,可以拷贝、可以赋值(赋值是重新绑定,不是修改被引用对象)。
第二,std::ref不延长生命周期。它只是保存地址,被引用对象什么时候销毁,引用就什么时候悬垂。标准用删除重载的方式阻止你对右值调用std::ref,但这只能挡住「明显绑临时量」的情况,挡不住「局部变量提前销毁」。
本文从接口、典型实现、使用场景三个层面拆解这两个函数,重点讲清它们与std::bind、std::thread的配合,以及那些编译能过但运行会出问题的边界。文中的实现是示意性的等价代码,用于说明语义,不代表任何标准库的具体源码;真实实现会涉及更多内部细节。
一、接口长什么样
标准给出的声明大致如下(省略了noexcept之外的一些细节):
// 适用于 C++17(示意声明)
namespace std {
template <class T> class reference_wrapper;
template <class T> reference_wrapper<T> ref(T& t) noexcept;
template <class T> reference_wrapper<T> ref(reference_wrapper<T> t) noexcept;
template <class T> reference_wrapper<const T> cref(const T& t) noexcept;
template <class T> reference_wrapper<const T> cref(reference_wrapper<T> t) noexcept;
// 阻止绑定右值
template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;
}几个要点。
ref的参数是T&,所以只能接受左值;传入 const 左值时T被推导为const X,返回reference_wrapper<const X>。- 对已经包装过的对象再
ref,返回的是同一个 wrapper(幂等),不会套两层。 cref总是加上 const,返回reference_wrapper<const T>。- 两个
= delete的重载专门接收右值,任何std::ref(42)、std::ref(std::string("x"))都会在编译期报错。这是标准库用重载决议做静态检查的典型手法。 - 函数都标了
noexcept,因为它们只做取地址和构造,不分配内存、不抛异常。
reference_wrapper的公开接口很少:
| 成员 | 作用 |
|---|
get() | 返回被引用对象的T& |
operator T&() | 隐式转换,让 wrapper 能传给形参为T&的函数 |
operator() | 当T可调用时,转发参数调用它(C++11 起) |
| 拷贝构造/拷贝赋值 | 拷贝指针 / 重新绑定 |
reference_wrapper(T&) | 构造函数,接受左值引用 |
注意reference_wrapper没有默认构造函数,也不能用右值构造。它是平凡可拷贝的(trivially copyable),大小通常就是一个指针。
二、等价实现:一个存指针的薄壳
理解源码最直接的方式是看它的等价结构。下面这段代码不是标准库源码,但语义上与主流实现一致。
// 适用于 C++17(示意实现)
#include <functional>
#include <memory>
#include <type_traits>
#include <utility>
template <class T>
class ReferenceWrapper {
public:
explicit ReferenceWrapper(T& t) noexcept
: ptr_(std::addressof(t)) {}
T& get() const noexcept { return *ptr_; }
operator T&() const noexcept { return *ptr_; }
template <class... Args>
auto operator()(Args&&... args) const
-> decltype(std::invoke(*ptr_, std::forward<Args>(args)...)) {
return std::invoke(*ptr_, std::forward<Args>(args)...);
}
private:
T* ptr_;
};
template <class T>
ReferenceWrapper<T> ref(T& t) noexcept {
return ReferenceWrapper<T>(t);
}三处细节值得说明。
为什么用std::addressof而不是&。如果类型重载了operator&,&t拿到的可能不是对象地址,而是一个重载函数的结果。std::addressof绕过重载,始终返回真实地址。标准库在需要可靠取址的地方都用它。
为什么operator T&要 const。wrapper 对象本身是 const 不影响被引用对象的可变性。const ReferenceWrapper<int>仍然应该能转换成int&并修改原对象——引用语义和 const 语义是两回事。
为什么operator()需要完美转发。当 T 是函数对象(比如 lambda)时,wrapper 要能代替它被调用。用std::forward保留实参的值类别,用std::invoke统一处理成员函数指针、成员数据指针和普通可调用对象。C++11 的实现用的是std::result_of,C++17 起改用std::invoke_result/decltype(std::invoke(...)),因为result_of在 C++17 被弃用、C++20 移除。
真实的reference_wrapper还会对T做remove_cv/remove_reference处理,并提供operator()仅在T可调用时参与的约束;不同标准库在 SFINAE 的写法上差别很大,但语义一致。
三、为什么需要它:按值传递的世界
C++ 的很多高层设施在传递参数时会按值拷贝,引用语义在那一层被抹掉了。这正是reference_wrapper存在的理由。
std::bind会 decay-copy 所有绑定的实参。std::bind(f, x)存的是x的副本,之后调用的是副本。要让它持有引用,必须写std::bind(f, std::ref(x))。
// 适用于 C++17
#include <functional>
#include <iostream>
void addTo(int& target, int delta) { target += delta; }
int main() {
int counter = 0;
// 下面这行若取消注释会编译失败:bind 保存的是 int 副本,无法绑定 int&
// auto byValue = std::bind(addTo, counter, 5);
auto byRef = std::bind(addTo, std::ref(counter), std::placeholders::_1);
byRef(7); // counter 变为 7
std::cout << counter << '\n';
auto byCRef = std::bind([](const int& v) { return v * 2; }, std::cref(counter));
std::cout << byCRef() << '\n';
return 0;
}被注释掉的那一行需要解释:std::bind会把counter拷贝进内部存储,调用addTo时传入的是一个右值副本,而addTo的第一个参数是int&,右值不能绑定到非 const 左值引用,于是编译报错。这恰恰说明std::bind默认按值保存。
std::thread的构造函数同样对实参做 decay-copy:线程对象内部保存的是参数的副本,等线程真正启动时才调用可调用对象。因此std::thread(f, x)修改的是副本,主线程看不到。传引用必须显式std::ref。
// 适用于 C++17
#include <functional>
#include <thread>
#include <vector>
void fill(std::vector<int>& out, int n) {
out.assign(n, 42);
}
int main() {
std::vector<int> data;
std::thread worker(fill, std::ref(data), 10);
worker.join(); // 必须 join 或 detach
// 此时 data.size() == 10
return 0;
}这里join()是必须的:std::thread析构时如果仍处于 joinable 状态,会调用std::terminate。而且join同时保证了「线程执行完毕」这一同步点,主线程之后读data才是安全的。
容器存引用。标准容器不能存引用类型,但可以存reference_wrapper。这让「一个 vector,元素是其它对象的别名」成为可能。
// 适用于 C++17
#include <algorithm>
#include <functional>
#include <vector>
int main() {
int a = 3, b = 1, c = 2;
std::vector<std::reference_wrapper<int>> refs{a, b, c};
std::sort(refs.begin(), refs.end(),
[](std::reference_wrapper<int> lhs,
std::reference_wrapper<int> rhs) {
return lhs.get() < rhs.get();
});
for (int& v : refs) {
v *= 10; // 通过 wrapper 的隐式转换修改原对象
}
return 0;
}排序交换的是 wrapper(也就是指针),原对象跟着「换位置」的错觉不存在——a、b、c的值没变,变的是refs里指针的顺序。这个例子也说明了 wrapper 的赋值语义:拷贝赋值让 wrapper 指向另一个对象,而不是把值写过去。
四、生命周期:唯一的真正风险
std::ref的设计里,标准只能挡住一种错误:绑定右值。其余的悬垂问题全靠程序员。
// 适用于 C++17(错误示例,仅用于说明风险)
#include <functional>
#include <thread>
std::thread makeWorker() {
int local = 0;
return std::thread([](int& v) { v = 1; }, std::ref(local)); // local 先销毁
}这段代码能通过编译(local是左值),但线程可能在local销毁之后才执行,访问的是已经结束生命周期的对象,属于未定义行为。标准库无法在编译期发现这种跨函数的生命周期问题,std::ref也不做任何运行期检查。
判断标准很简单:被引用对象的生命周期必须严格覆盖 wrapper 被使用的整个区间。在线程场景里,这意味着join()必须发生在对象销毁之前;在异步场景里,意味着要确保回调执行完再释放资源。std::async配合std::ref尤其危险,因为std::async返回的 future 如果不等待,任务可能跑到对象销毁之后。
另一个隐蔽的坑是把std::ref传给按值接收的模板函数。比如template <class T> void g(T t),传入std::ref(x)时T被推导为reference_wrapper<int>,函数内部拿到的是 wrapper 的副本(很便宜,但仍是指针),只有发生到int&的转换时才真正接触原对象。这不是错误,但要清楚拷贝的是 wrapper 而非int。
常见坑点
- ❌
std::ref(42)或std::ref(std::string("tmp"))。
✅ 右值重载被= delete,编译期就会报错;先存成具名左值再ref。
- ❌ 把局部变量的
std::ref交给线程/异步任务,局部变量先销毁。
✅ 保证join()或 future 等待发生在对象生命周期内。
- ❌ 认为
std::ref会延长对象寿命,或认为它做了拷贝。
✅ 它只存指针,不拥有对象、不延长寿命。
- ❌
std::bind(f, x)以为传了引用,实际传了副本。
✅ 需要引用语义时写std::bind(f, std::ref(x));能用 lambda 时优先 lambda。
- ❌ 对
const对象用std::ref,之后试图修改。
✅ref会推导出reference_wrapper<const T>;要只读就用cref,要可写就别加 const。
- ❌ 容器里存
reference_wrapper,被引用对象先于容器销毁。
✅ 明确所有权,容器不拥有元素;必要时改用std::reference_wrapper之外的拥有型容器。
- ❌ 把
reference_wrapper当成线程安全的共享机制。
✅ 它只提供引用语义,并发读写仍需互斥量或原子操作。
- ❌ 对重载了
operator&的类型自己写&t取地址再包装。
✅ 用std::ref/std::cref,内部走std::addressof。
总结
| 项目 | std::ref | std::cref |
|---|
| 返回类型 | reference_wrapper<T> | reference_wrapper<const T> |
| 接受右值 | 否(删除重载) | 否(删除重载) |
| 是否拷贝对象 | 否,只存地址 | 否,只存地址 |
| 是否延长生命周期 | 否 | 否 |
| 典型用途 | bind/thread传引用、容器存引用 | 只读传递、避免拷贝 |
std::ref与std::cref的本质,是在「按值传递」的设施里塞进一个「按引用访问」的小对象。它的实现只有一个指针和几个转发函数,复杂度全部集中在生命周期管理上。用之前先问一句:被引用的对象,能不能保证活到 wrapper 最后一次被使用?答不上来,就不要用。