std::reference_wrapper 的通俗易懂解释
- 一、简介
- 二、std::reference_wrapper 的初衷
- 三、常用示例
- 3.1、与 `make_pair` 和 `make_tuple` 一起使用
- 3.2、引用容器
- 3.3、通过 `std::thread` 按引用传递参数给启动函数
- 3.4、引用作为类成员
- 3.5、按引用传递函数对象
- 3.6、与绑定表达式一起使用
- 四、总结
- 五、推荐阅读
一、简介
std::reference_wrapper<T>
是一个可复制、可赋值的对象,它模拟了引用(T&
)。它提供了引用不可为空的保证,以及类似指针的灵活性,可以重新绑定到另一个对象。
通常使用 std::ref
(或 用于 reference_wrapper<const T>
的std::cref
)来创建 std::reference_wrapper<T>
。
例如:
template<typename N>
void change(N n) {
// 如果 n 是 std::reference_wrapper<int>,它会在这里隐式转换为 int&。
n += 1;
}
void foo() {
int x = 10;
int& xref = x;
change(xref); // 按值传递
// x 仍然是 10
std::cout << x << "\n"; // 10
// 显式地按引用传递
change<int&>(x);
// x 现在是 11
std::cout << x << "\n"; // 11
// 或者使用 std::ref
change(std::ref(x)); // 按引用传递
// x 现在是 12
std::cout << x << "\n"; // 12
}
二、std::reference_wrapper 的初衷
在 C++ 中,尽可能地使用引用而不是指针,因为引用不能为 null,可以明确地表达按引用传递的意图,并且可读性更好,因为没有解引用(*
,->
)的混乱。
但是,引用是对象的顽固别名。这带来了一些挑战,比如上面的例子,函数模板参数类型必须显式指定才能按引用传递。
此外,通常情况下无法获取引用本身的地址,因为根据 C++ 标准,引用不是对象(存储区域)。在 C++ 中,声明对引用的引用、引用的数组以及指向引用的指针都是禁止的。因此,引用类型不满足 STL 容器元素的 Erasable 要求。因此,不能拥有包含引用元素的容器(例如,vector
或 list
):
std::vector<int&> v; // 错误
此外,引用不能重新绑定到另一个对象。因此,将引用赋值给另一个引用不会赋值引用本身;它会赋值对象:
int x=10, y=20;
int &xref = x, &yref = y;
xref = yref;
// xref 仍然引用 x,现在是 20。
由于引用不能重新绑定,因此将引用作为类成员会很麻烦,因为引用成员会使类不可赋值——默认的复制赋值运算符会被删除。移动语义对于引用成员来说根本没有意义。
std::reference_wrapper
是一个可复制、可赋值的对象,它模拟了引用。与它的名字相反,它不包装引用。它通过封装一个指针(T*
)并隐式转换为引用(T&
)来工作。它不能进行默认构造或用临时对象初始化;因此,它不能为 null
或无效:
std::reference_wrapper<int> nr; // 错误!必须初始化。
std::string str1{"Hello"};
std::string str2{"World"};
auto r1 = std::ref(str1); // OK
auto r2 = std::ref(str2); // OK
// 赋值会重新绑定 reference_wrapper
r2 = r1; // r2 现在也引用 str1
// 隐式转换为 std::string&
std::string cstr = r2; // cstr 是 "Hello"
// 可以创建 reference_wrapper 的数组
std::reference_wrapper<std::string> arr[] = {str1, str2};
auto r2 = std::ref(std::string("Hello")); // 错误!不允许使用临时对象(右值)。
唯一的缺点是,要访问对象的成员(T
),必须使用 std::reference_wrapper<T>::get
方法:
std::string str{"Hello"};
auto sref = std::ref(str);
// 打印 str 的长度
std::cout << sref.get().length() << "\n"; // 5
此外,要为被引用的对象赋值,请使用 get()
:
sref.get() = "World"; // str 被更改为 "World"
std::cout << str << "\n"; // World
三、常用示例
3.1、与 make_pair
和 make_tuple
一起使用
std::reference_wrapper
可以用作模板函数(或构造函数)的参数,以避免显式指定模板参数类型。这里一个特殊的例子是 make_pair
(make_tuple
),它的目的是减少与实例化对(元组)相关的冗长性。比较以下两种方式:
int m=10, n=20;
std::string s{"Hello"};
std::pair<int&, int> p1(m, n);
std::tuple<int&, int, std::string&> t1(m, n, s);
// 与以下方式对比
auto p2 = std::make_pair(std::ref(m), n);
auto t2 = std::make_tuple(std::ref(m), n, std::ref(s));
reference_wrapper
的另一个优点是它不能用临时对象实例化。例如,以下代码会导致未定义行为,因为临时对象的生存期仅延长到构造函数参数超出范围为止:
std::string yell() { return "hey"; }
std::pair<const std::string&, int> p3(yell(), n); // 错误!
// 临时对象 std::string("hey") 这里已经被销毁。
// 访问 p3.first 这里会导致 UB。
以上情况属于悬空引用,可以使用 reference_wrapper
避免:
// 但是,使用 std::ref 是安全的
auto p4 = std::make_pair(std::ref(yell()), n); // 错误!很好
make_pair
和 make_tuple
在某种程度上与 C++17 的 无关。但是,这些原因仍然适用,现在,使用构造函数:
由于C++17引入了类模板参数推断 (CTAD),所以make_pair
和make_tuple
的作用已经不再那么重要了。但是,仍然适用于构造函数:
std::pair p5(std::ref(m), n);
std::tuple t3(std::ref(m), n, std::ref(s));
然而,make_pair
和make_tuple
与CTAD构造的pair
和tuple
之间存在细微的差别(在大多数情况下这并不重要)。make_pair
和make_tuple
将reference_wrapper<T>
转换为引用(T&)
,而CTAD构造的pair
和tuple
则不是这样。
3.2、引用容器
与引用不同,reference_wrapper
是一个对象,因此满足 STL 容器元素的要求(准确地说是 满足Erasable 要求)。因此,reference_wrapper
可以用作向量元素类型。
reference_wrapper<T>
可以作为指针类型(T*
)的安全替代方案,用于存储在向量中:
using namespace std;
vector<reference_wrapper<int>> v; // OK
int a=10;
v.push_back(std::ref(a));
3.3、通过 std::thread
按引用传递参数给启动函数
可以通过 std::thread(startFunction, args)
在创建新线程时将参数传递给启动函数。这些参数按值从线程创建函数传递,因为 std::thread
构造函数会在将参数传递给启动函数之前复制或移动创建者的参数。
因此,启动函数的引用参数不能绑定到创建者的参数。它只能绑定到 std::thread
创建的临时对象:
void start(int& i) { i += 1; }
void start_const(const int& i) { }
void create() {
int e = 10;
// e 在下面被复制到一个临时对象
std::thread(start, e).join(); // 错误!不能将临时对象绑定到 int&。
std::thread(start_const, e).join(); // OK。但实际上是按值传递
}
如果想按引用将参数传递给启动函数,可以通过 std::ref
来实现,如下所示:
void create() {
int e = 10;
std::thread(start, std::ref(e)).join(); // OK。按引用传递
// e 现在是 11
std::thread(start_const, std::ref(e)).join(); // 按引用传递
std::thread(start_const, std::cref(e)).join(); // 按引用传递
}
在上面,std::ref
会生成一个 reference_wrapper<int>
,它最终会隐式转换为 int&
,从而将 start(int&)
的引用参数绑定到 create()
传递的参数。
3.4、引用作为类成员
将引用作为类成员会带来一些问题,例如,它会使类不可赋值,并且实际上不可移动:
struct W {
W(int& i):iRef(i) {}
int& iRef;
};
int u=10, v=20;
W w1(u);
W w2(v);
w1 = w2; // 错误!隐式删除的复制赋值运算符
通常的做法是避免将引用作为类成员,而是使用指针。
reference_wrapper
提供了二者的最佳组合:
struct W {
W(int& i):iRef(i) {}
std::reference_wrapper<int> iRef;
};
W w3(u);
W w4(v);
w3 = w4; // OK
3.5、按引用传递函数对象
只要 T
是可调用对象,std::reference_wrapper<T>
就可以像函数一样被调用。如果想避免复制大型或有状态的函数对象,这个特性在 STL 算法中特别有用。
此外,T
可以是任何可调用对象——普通函数、lambda 表达式或函数对象。例如:
struct Large {
bool operator()(int i) const {
// 过滤和处理
return true;
}
// 大量数据
};
const Large large; // 大量不可变数据和函数对象
std::vector<int> in1; // 输入向量
std::vector<int> in2; // 输入向量
void process() {
std::vector<int> out;
// 按引用传递 Large 以避免复制
std::copy_if(in1.begin(), in1.end(), std::back_inserter(out), std::ref(large));
std::copy_if(in2.begin(), in2.end(), std::back_inserter(out), std::ref(large));
// 使用过滤后的 'out' 向量
}
3.6、与绑定表达式一起使用
std::bind
会生成一个可调用包装器,称为绑定表达式,它会将调用转发到包装的可调用对象。绑定表达式可以绑定包装的可调用对象的部分或全部参数。
但是,绑定参数在绑定表达式中会被复制或移动。
void caw(const std::string& quality, const std::string& food) {
std::cout << "A " << quality << " " << food << "\n";
}
using namespace std::placeholders; // for _1, _2
std::string donut("donut");
auto donutcaw = std::bind(caw, _1, donut); // donut 被复制
donutcaw("chocolate"); // A chocolate donut
因此,如果想按引用传递绑定参数,需要使用 std::ref
(或 std::cref
):
std::string muffin("muffin");
auto muffincaw = std::bind(caw, _1, std::ref(muffin)); // muffin 按引用传递
muffincaw("delicious"); // A delicious muffin
四、总结
std::reference_wrapper
是一个强大的工具,它扩展了 C++ 中引用的功能,使其更灵活、更安全。它可以用于:
- 避免悬空引用: 它不能用临时对象初始化,因此可以有效地防止悬空引用问题。
- 创建引用容器:
std::reference_wrapper
是一个对象,可以作为 STL 容器的元素类型,从而允许创建引用容器。 - 解决引用作为类成员的难题:
std::reference_wrapper
可以作为类成员,使类可赋值,避免了引用成员带来的限制。 - 按引用传递函数对象:
std::reference_wrapper
可以用来按引用传递函数对象,避免了复制大型或有状态的函数对象。 - 与绑定表达式一起使用:
std::reference_wrapper
可以与std::bind
一起使用,以按引用传递绑定参数。
五、推荐阅读
- C++
std::reference_wrapper
官方说明(cppreference)。 reference_wrapper
的提案。