☰
解析C++11的std::ref、std::cref源码
2026/10/22 8:01:10 网站建设 项目流程

前言


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。


常见坑点



  1. ❌std::ref(42)或std::ref(std::string("tmp"))。


✅ 右值重载被= delete,编译期就会报错;先存成具名左值再ref。



  1. ❌ 把局部变量的std::ref交给线程/异步任务,局部变量先销毁。


✅ 保证join()或 future 等待发生在对象生命周期内。



  1. ❌ 认为std::ref会延长对象寿命,或认为它做了拷贝。


✅ 它只存指针,不拥有对象、不延长寿命。



  1. ❌std::bind(f, x)以为传了引用,实际传了副本。


✅ 需要引用语义时写std::bind(f, std::ref(x));能用 lambda 时优先 lambda。



  1. ❌ 对const对象用std::ref,之后试图修改。


✅ref会推导出reference_wrapper<const T>;要只读就用cref,要可写就别加 const。



  1. ❌ 容器里存reference_wrapper,被引用对象先于容器销毁。


✅ 明确所有权,容器不拥有元素;必要时改用std::reference_wrapper之外的拥有型容器。



  1. ❌ 把reference_wrapper当成线程安全的共享机制。


✅ 它只提供引用语义,并发读写仍需互斥量或原子操作。



  1. ❌ 对重载了operator&的类型自己写&t取地址再包装。


✅ 用std::ref/std::cref,内部走std::addressof。


总结




项目std::refstd::cref



返回类型reference_wrapper<T>reference_wrapper<const T>

接受右值否(删除重载)否(删除重载)

是否拷贝对象否,只存地址否,只存地址

是否延长生命周期否否

典型用途bind/thread传引用、容器存引用只读传递、避免拷贝



std::ref与std::cref的本质,是在「按值传递」的设施里塞进一个「按引用访问」的小对象。它的实现只有一个指针和几个转发函数,复杂度全部集中在生命周期管理上。用之前先问一句:被引用的对象,能不能保证活到 wrapper 最后一次被使用?答不上来,就不要用。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询