为什么需要引用封装
先看一段代码:
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); 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(); }
|
如果上面代码中不加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::cout << "Total: " << c.count << std::endl;
std::for_each(data.begin(), data.end(), 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));
|
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(); }
|
实现原理
先前说过,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 不会延长原对象的生命周期。ref 和 cref 都只能绑定左值。绑定右值编译器会报错。
注意
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) { }
int main() { std::vector<int> data(100000000); std::fill(data.begin(), data.end(), 42);
auto start1 = std::chrono::high_resolution_clock::now(); std::for_each(data.begin(), data.end(), print); auto end1 = std::chrono::high_resolution_clock::now(); 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
|