c++中ref的作用示例解析
发布时间:2026/11/4 8:57:54 锦皓数字建站

前言std::ref是functional里的一个函数模板它返回std::reference_wrapperT对象。这个对象的行为很像引用可以隐式转换成T可以被函数按值接收后仍然指向原来的对象但它又和引用不同——它可以被拷贝、被赋值、被存进容器。正是这个「像引用又能拷贝」的特性让它成为标准库里一个不太起眼、却经常救命的小工具。初学者常有的误解是把它当成「给变量起个别名」的语法糖觉得和T差不多。实际上两者解决的问题完全不同T是语言层面的引用一旦遇到「按值传递」的模板参数就会退化——函数模板推导出T int形参按值拷贝引用语义丢失std::ref则是把「引用」包装成一个值对象让它在按值传递的过程中被完整保留下来。另一类误解是以为std::ref能解决所有「传引用」的问题。它不能延长生命周期也不能提供任何线程安全保障。绑定的对象一旦销毁reference_wrapper立刻悬垂多个线程通过std::ref共享同一个变量而不加锁仍然是数据竞争。本文先讲清楚std::ref返回的是什么、为什么需要它再给四个典型场景的代码std::thread传参、std::bind绑定、std::for_each保留函数对象状态、容器与元组存储引用。最后单独讲生命周期和线程安全这两个边界。一、std::ref 返回的是什么std::ref(x)返回一个std::reference_wrapperT其中T由x推导而来x是左值时T是去掉引用后的类型。这个类模板大致只有三样东西一个指向T的指针成员operator T()转换运算符让它可以隐式变成引用get()成员函数显式取出T。此外它还有operator()可以把调用转发给被包装的可调用对象——这就是为什么std::ref包装的函数对象可以直接当函数用。// 适用于 C17#include functional#include iostreamint main() {int value 10;std::reference_wrapperint r std::ref(value);r.get() 20; // 通过 get() 修改原变量int alias r; // 隐式转换回 intalias 30;std::cout value \n; // 按语义应当是 30return 0;}关键点在于reference_wrapper是一个类类型它的拷贝构造只是复制内部指针所以拷贝之后仍然指向同一个对象。这与T有本质区别——T本身不可拷贝、不可赋值、不可默认构造因此无法作为std::vector的元素类型也无法被按值传参的模板保存下来。需要区分std::ref与std::cref前者返回reference_wrapperT可以修改后者返回reference_wrapperconst T只能读。对 const 对象必须用std::cref用std::ref会导致编译错误因为reference_wrapperconst T不能绑定到需要T的场合。二、问题的根源模板按值拷贝标准库的很多组件在接收可调用对象或参数时采用的是「按值」语义std::thread的构造函数会把每个实参 decay-copy 到内部存储再在新线程里以左值形式传给目标函数std::bind会按值保存绑定的实参std::for_each、std::sort等算法按值接收函数对象std::make_pair、std::make_tuple会对实参做 decay。按值语义的设计初衷是安全避免悬垂引用让组件拥有自己的副本。但它带来一个副作用——如果你希望函数修改调用者的变量副本做不到。std::ref正是为这个缝隙准备的把引用包装成一个可拷贝的值按值传递时复制的是「指向原对象的包装器」而不是对象本身。// 适用于 C17#include functional#include iostreamvoid addTo(int target, int delta) {target delta;}int main() {int counter 0;// 直接传 counterbind 会拷贝一份修改不会反映到 counterauto byValue std::bind(addTo, counter, 1);byValue();std::cout counter \n; // 按语义应当是 0// 用 std::ref 包装bind 拷贝的是包装器指向同一个 counterauto byRef std::bind(addTo, std::ref(counter), 1);byRef();std::cout counter \n; // 按语义应当是 1return 0;}第一段之所以输出 0是因为std::bind保存的是counter的副本调用addTo时传进去的是那个副本的引用改的是副本。第二段用std::ref之后保存的是reference_wrapperint调用时隐式转换成counter的引用改的才是原变量。三、四个典型场景3.1 std::thread 传参std::thread的构造函数对实参做 decay-copy这是新手最容易踩的坑写std::thread t(worker, data)期望worker修改data实际上线程拿到的是副本。// 适用于 C17#include functional#include iostream#include thread#include vectorvoid sumAll(const std::vectorint data, long long out) {long long total 0;for (int v : data) total v;out total;}int main() {std::vectorint data{1, 2, 3, 4, 5};long long result 0;std::thread t(sumAll, std::cref(data), std::ref(result));t.join();std::cout result \n; // 按语义应当是 15return 0;}std::cref(data)避免了对整个vector的拷贝同时保证线程内只读std::ref(result)让线程写入的是main里的result。这里必须join()否则main结束时result和data都已销毁线程还在访问它们属于未定义行为。3.2 std::bind 绑定成员与参数std::bind保存绑定的实参时同样按值拷贝。需要保留引用语义时绑定点要用std::ref// 适用于 C17#include functional#include iostreamstruct Accumulator {int total 0;void add(int v) { total v; }};int main() {Accumulator acc;auto addTen std::bind(Accumulator::add, std::ref(acc), 10);addTen();std::cout acc.total \n; // 按语义应当是 10return 0;}绑定成员函数时第一个参数是对象。如果写成std::bind(Accumulator::add, acc, 10)acc会被拷贝修改的是副本。这里std::ref(acc)保证调用作用在原对象上。不过要说明的是C11 之后lambda 通常是比std::bind更好的选择。std::bind的占位符、重载解析、嵌套绑定都会让错误信息变得难读而 lambda 的捕获列表能直接表达「按值」还是「按引用」。只有在需要把绑定结果当值传递、或者兼容旧代码时std::bind才更合适。3.3 std::for_each 保留函数对象状态std::for_each的签名是「按值接收函数对象并返回它的副本」。如果传入一个有状态的函数对象算法内部累加的状态在返回后就被丢弃了——除非显式接住返回值或者用std::ref// 适用于 C17#include algorithm#include functional#include iostream#include vectorstruct Counter {int n 0;void operator()(int) { n; }};int main() {std::vectorint data{1, 2, 3, 4};Counter c;std::for_each(data.begin(), data.end(), std::ref(c));std::cout c.n \n; // 按语义应当是 4return 0;}这里std::ref(c)让算法操作的就是c本身。等价的写法是c std::for_each(data.begin(), data.end(), c);——接住返回值把算法内部的副本赋回来。两种写法都可以std::ref更直接接返回值更符合「函数式」的风格。3.4 容器与元组中存储引用引用不能作为容器元素但reference_wrapper可以// 适用于 C17#include functional#include iostream#include vectorint main() {int a 1, b 2, c 3;std::vectorstd::reference_wrapperint refs{std::ref(a), std::ref(b), std::ref(c)};for (int v : refs) v * 10;std::cout a b c \n; // 按语义应当是 10 20 30return 0;}for (int v : refs)能成立是因为reference_wrapperint可以隐式转换成int。同样的技巧也可以用在std::tuple、std::pair上——std::make_tuple(std::ref(a), std::ref(b))得到的是「引用元组」修改它会作用到原变量。std::tie(a, b)是另一条路径它直接构造引用元组语义相近但用法不同。四、生命周期与线程安全reference_wrapper不持有对象也不延长生命周期。它内部只有一个指针被包装的对象什么时候销毁它就在什么时候悬垂。这一点和裸指针完全一样编译器不会替你做任何检查。因此有三条硬规则不能包装临时对象。标准为std::ref提供了一个接受const T的被删除重载std::ref(42)、std::ref(makeObject())都会在编译期失败。这是有意的保护不是缺陷。不要让包装器的寿命超过被包装对象。把std::ref(local)存进一个生命周期更长的容器或线程是最常见的悬垂来源编译器对这种情况无能为力。跨线程共享必须同步。std::ref只是让两个线程看到同一个变量它不提供任何原子性或互斥。一个线程写、另一个线程读而不加锁是数据竞争属于未定义行为不是「可能读到旧值」这么轻。// 适用于 C17反面示例缺少同步不要照抄#include functional#include threadint shared 0;void worker() {for (int i 0; i 1000; i) shared; // 数据竞争}int main() {std::thread t1(worker);std::thread t2(worker);t1.join();t2.join();return 0;}这段代码与std::ref无关但它说明了一个常被混淆的点std::ref解决的是「值语义导致的拷贝」不是「并发访问的同步」。后者要用std::atomic、std::mutex。上面的shared应该改成std::atomicint或者用互斥量保护。常见坑点1. 以为 std::thread 会自动传引用❌std::thread t(worker, data);期望线程修改data✅std::thread t(worker, std::ref(data));线程构造函数对实参做 decay-copy。不加std::ref线程拿到的是副本函数返回后副本销毁原变量没有任何变化。2. 让被包装对象先于线程销毁❌std::thread t; { std::string s abc; t std::thread(f, std::ref(s)); } t.join();✅ 把s声明在join()之后才离开的作用域保证线程运行期间它一直存活std::ref不做生命周期管理。花括号结束时s已销毁线程仍持有它的引用后续访问是未定义行为。注意std::ref本身拒绝右值所以直接写std::ref(std::string(abc))是编译错误真正危险的是「先绑定到局部变量、再让局部变量提前死亡」这种写法。3. 以为 std::ref 返回 T❌auto r std::ref(x);然后期待r是int✅auto r std::ref(x);或int r std::ref(x);std::ref返回的是reference_wrapperT这个值对象auto推导出的是对它的引用类型不是T。需要真正的引用时让隐式转换发生int r std::ref(x);。4. 忘记 reference_wrapper 可以被重新赋值❌auto r std::ref(a); r std::ref(b);以为会报错 ✅ 明确知道这一步是「换绑」r现在指向b与真正的引用不同reference_wrapper的赋值运算符是「重新绑定」语义不会修改被引用的对象。这个行为和直觉相反读代码时容易误判。5. 在 std::bind 中漏掉 std::ref❌auto f std::bind(addTo, counter, 1);✅auto f std::bind(addTo, std::ref(counter), 1);std::bind按值保存绑定参数不加std::ref就是操作副本。这类错误不会报错只会「结果不对」很难排查。6. 用 std::ref 共享数据却不加锁❌ 两个线程都通过std::ref(counter)写入同一个int✅ 把类型换成std::atomicint或用std::mutex保护std::ref只负责让两个线程看到同一个对象不负责让访问变得安全。无同步的并发读写是数据竞争标准规定为未定义行为。7. 用 std::ref 绑定 const 对象❌const int c 1; auto r std::ref(c);然后试图修改 ✅auto r std::cref(c);且只读使用std::ref推导出reference_wrapperconst int无法转换成int任何写操作都会编译失败。要只读共享直接用std::cref表达意图更清楚。8. 把 reference_wrapper 当成所有权工具❌ 用std::vectorstd::reference_wrapperWidget保存对象指望它管理生命周期 ✅ 需要所有权时用std::vectorstd::unique_ptrWidget或std::shared_ptrreference_wrapper只借用不拥有。容器销毁时不会销毁被引用的对象被引用对象先销毁时容器里留下的就是悬垂引用。总结要点说明本质std::ref(x)返回reference_wrapperT一个可拷贝的引用包装器解决的问题模板/组件按值拷贝实参时保留引用语义常见场景std::thread传参、std::bind绑定、std::for_each保留状态、容器存引用只读版本std::cref返回reference_wrapperconst T生命周期不延长寿命被包装对象销毁后立即悬垂并发不提供同步共享写入仍需原子操作或互斥量替代方案多数场景下 lambda 比std::bind更清晰std::ref的价值可以用一句话概括它让「引用」变成了一个可以按值传递的值。理解这一点就能判断什么时候该用它——凡是「组件会拷贝实参但我希望它操作原对象」的场合都是它的用武之地凡是涉及生命周期和并发安全的场合它都帮不上忙也不该指望它。真正写现代 C 时lambda 的引用捕获往往比std::bind加std::ref更直观但在std::thread和多线程回调这些接口已经定死的地方std::ref仍然是最直接的表达方式。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。