std::ref和std::cref
LDK Lv4

为什么需要引用封装

先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <thread>

void increment(int &x) {
x++;
}

int main() {
int a = 5;
std::thread t(increment, a); // ❌ 编译失败
// std::thread t(increment, std::ref(a)); // 改成这样就能编译成功
t.join();
return 0;
}

上面函数为什么会编译失败呢?

因为increment(int&)函数需要引用类型的参数,而std::thread默认按值复制参数,它尝试将变量a拷贝一份传递给increment,这和期待的引用类型不一样,然后编译器报错。

std::ref和std::cref是什么

  • std::ref(obj):返回一个可修改引用的包装器。
  • std::cref(obj):返回一个const引用的包装器。

这两个函数本质上返回 std::reference_wrapper<T> 类型,它可以模拟“按引用传参”的行为,但仍然以“按值”方式传递给调用者。其函数定义长这样:

1
2
3
4
template <typename _Tp>
_GLIBCXX20_CONSTEXPR inline std::reference_wrapper<_Tp> ref(_Tp &__t) noexcept {
return reference_wrapper<_Tp>(__t);
}

典型使用场景

1. std::thread

1
2
3
4
5
6
7
8
9
10
11
12
#include <thread>
#include <iostream>

void print(int& x) {
std::cout << x << std::endl;
}

int main() {
int a = 42;
std::thread t(print, std::ref(a)); // ✅ 按引用传入
t.join();
}

如上面所述,如果不加std::ref会报错。

2. std::bind

这是个大坑

1
2
3
4
5
6
7
8
9
10
11
#include <functional>

void set_to_100(int& x) {
x = 100;
}

int main() {
int a = 0;
auto f = std::bind(set_to_100, std::ref(a));
f(); // ✅ 成功修改 a
}

如果上面代码中不加std::ref,那么传参时变量a就会被拷贝,f执行时内部修改的就是拷贝后的变量而不是原变量。你就说坑不坑吧。

传递函数对象

1. 传递有状态的函数对象时
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Counter {
int count = 0;
void operator()(int x) {
count += x;
}
};

Counter c;
std::for_each(data.begin(), data.end(), std::ref(c)); // 使用std::ref保持状态
std::cout << "Total: " << c.count << std::endl; // 能访问到累计值

// 而普通传递会拷贝,每个元素使用不同的Counter实例
std::for_each(data.begin(), data.end(), c); // 错误:不会修改原始c
2. 传递不可拷贝的函数对象
1
2
3
4
5
6
7
8
9
struct NonCopyableFunctor {
NonCopyableFunctor() = default;
NonCopyableFunctor(const NonCopyableFunctor&) = delete;
void operator()(int x) { std::cout << x << " "; }
};

NonCopyableFunctor f;
std::for_each(data.begin(), data.end(), std::ref(f)); // 唯一可行方式
// std::for_each(data.begin(), data.end(), f); // 错误,不可拷贝
3. 需要共享状态的多处调用
1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger {
std::mutex mtx;
std::ofstream log_file;
public:
void operator()(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx);
log_file << msg << std::endl;
}
};

Logger logger;
std::thread t1([&] { std::for_each(data1.begin(), data1.end(), std::ref(logger)); });
std::thread t2([&] { std::for_each(data2.begin(), data2.end(), std::ref(logger)); });
4. std::cref的使用场景
1
2
3
4
5
6
7
8
9
void show(const int& x) {
std::cout << x << std::endl;
}

int main() {
int val = 10;
auto f = std::bind(show, std::cref(val));
f(); // 输出 10
}

实现原理

先前说过,std::ref()函数的返回值是:reference_wrapper<_Tp>(__t),其本质上是一个指针模拟引用:

1
2
3
4
5
6
7
8
9
template<typename T>
class reference_wrapper {
public:
explicit reference_wrapper(T& ref) noexcept : ptr(std::addressof(ref)) {}
T& get() const noexcept { return *ptr; }
operator T&() const noexcept { return *ptr; }
private:
T* ptr;
};

上述代码是精简过的,删除了源码中的很多函数,但不影响理解原理。

从代码中可以看到,reference_wrapper内部的指针指向的就是被包装的对象。这意味着两件事:

  • reference_wrapper 不会延长原对象的生命周期
  • refcref只能绑定左值。绑定右值编译器会报错。

注意

std::ref包装函数时是有性能损耗的,参照下面测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <chrono>
#include <iostream>
#include <vector>
#include <algorithm>

void print(int x) {
// 空函数体以减少IO开销
}

int main() {
std::vector<int> data(100000000); // 一亿个int
std::fill(data.begin(), data.end(), 42);

// 测试1:函数指针
auto start1 = std::chrono::high_resolution_clock::now();
std::for_each(data.begin(), data.end(), print);
auto end1 = std::chrono::high_resolution_clock::now();

// 测试2:std::ref
auto start2 = std::chrono::high_resolution_clock::now();
std::for_each(data.begin(), data.end(), std::ref(print));
auto end2 = std::chrono::high_resolution_clock::now();

std::cout << "函数指针时间: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count()
<< "ms\n";
std::cout << "std::ref时间: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count()
<< "ms\n";
}

输出:

1
2
函数指针时间: 142ms
std::ref时间: 646ms
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 74.8k 访客数 访问量