std::ref是包装左值引用的可拷贝对象,用于在std::thread或std::bind中传递引用语义;它不延长对象生命周期,要求被包装变量在整个调用期间有效,且形参必须声明为引用类型。

std::ref 本质是包装引用的可拷贝对象
std::ref 不是“传递引用”本身,而是把一个左值引用包装成一个可拷贝、可移动的对象,从而绕过 std::thread 或 std::bind 默认的值语义拷贝行为。它内部持有原始对象的指针(或引用),解引用时才真正访问原变量。
常见错误是以为 std::ref(x) 让线程“拥有”了 x 的生命周期 —— 实际上它不延长生存期,若原变量在子线程执行前销毁,访问就是未定义行为。
- 必须确保被
std::ref包装的变量在整个绑定对象或线程运行期间持续有效 -
std::ref只接受左值(即命名变量),不能用于字面量或临时对象:std::ref(42)编译失败 - 对应还有
std::cref,用于只读引用(返回const T&)
用 std::ref 传引用给 std::thread
默认情况下,std::thread 构造函数会对所有参数做拷贝(调用拷贝构造),即使你传的是引用类型,也会拷出一份副本。要让线程函数真正修改外部变量,必须显式用 std::ref 包装。
int value = 10;
auto func = [](int& v) { v *= 2; };
std::thread t(func, std::ref(value));
t.join();
// 此时 value == 20
注意:如果线程函数参数声明为 int(非引用),哪怕你传 std::ref(value),也会触发 int& → int 的隐式转换并拷贝值,失去引用效果。参数类型必须匹配。
立即学习“C++免费学习笔记(深入)”;
- 线程函数形参必须声明为引用类型(如
int&、std::string&) - 传参时用
std::ref(var),不是&var(后者传指针,且无法通过类型推导自动转引用) - 避免在 lambda 捕获列表中用
[&]试图“共享”变量 —— 捕获的是创建线程时的栈上下文,子线程可能已析构
std::ref 在 std::bind 中的必要性
std::bind 同样默认按值保存参数。即使你 bind 一个引用参数的函数,不加 std::ref,绑定对象里存的仍是副本。
void increment(int& x) { ++x; }
int a = 5;
auto bound = std::bind(increment, std::ref(a)); // ✅ 修改 a
bound();
// a == 6
auto bound_bad = std::bind(increment, a); // ❌ 拷贝 a,increment 修改的是副本
bound_bad();
// a 仍是 5
另一个典型场景是绑定成员函数时传引用参数:
struct Counter { int val = 0; void add(int& delta) { val += delta; } };
Counter c;
int step = 3;
std::bind(&Counter::add, &c, std::ref(step))(); // step 被引用传入
-
std::bind对std::ref的处理是特化的:调用时自动解引用,还原为原始引用 - 不要对
std::ref做二次包装(如std::ref(std::ref(x))),编译器通常拒绝或行为未定义 - C++17 起推荐优先用
std::thread直接构造 + lambda 捕获,比std::bind更直观;但遗留代码或需延迟绑定时仍需std::ref
std::ref 的替代方案与边界情况
并非所有“传引用”需求都该用 std::ref。有些场景更安全或更自然:
- 用指针(
&x)+ 函数参数为T*:明确所有权和空值风险,适合需要判空或可选修改的逻辑 - 用
std::shared_ptr管理对象生命周期:当多个线程需共享并延长对象生存期时,比裸引用 +std::ref更健壮 - lambda 按引用捕获(
[&x]):仅适用于线程立即启动且父作用域生命周期可控的情况;不可用于存储到容器或延迟调用
std::ref 是个轻量包装器,不参与内存管理,也不做线程同步。如果你在线程间通过 std::ref 共享变量,务必自行加锁(如 std::mutex)或使用原子类型,否则数据竞争依然存在。










