C++引用包装

Share on:
600 Words | Read in about 3 Min | View times

Overview

现代C++引入一个std::ref()用来模拟某个变量的引用。C++原来就有引用,为什么还需要模拟引用呢?这是为了解决某些场景下,只能值传递传参,却需要对传入的参数进行修改的问题。这类问题往往出现在函数式编程里。

理解std::ref()之前还需要先了解一个引用包装(reference wrapper)的概念,std::ref()的返回值就是一个引用包装。

本文围绕<refwrap>头文件提供的std::reference_wrapperstd::ref的实现细节,来说明为什么需要这项技术。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

为什么需要std::ref

在函数式编程中,如std::bind绑定一个函数,对于需要传入函数的参数,统一使用值传递的方式传参,而不是引用传递的方式传参。即使参数本身就是一个左值引用也不行,而通过将参数用std::ref()包装之后再传参,就能得到像引用一样的效果:

 1void foo(int& a, int& b) {
 2    std::cout << "call f()" << std::endl;
 3    ++a;
 4    ++b;
 5    std::cout << "a: " << &a << ", " << a << std::endl;
 6    std::cout << "b: " << &b << ", " << b << std::endl;
 7}
 8
 9int main() {
10    int a = 0, b = 0;
11    int& ra = a;
12
13    auto f = std::bind(foo, ra, std::ref(b));
14
15    std::cout << "before call f()" << std::endl;
16    std::cout << "a: " << &a << ", " << a << std::endl;
17    std::cout << "b: " << &b << ", " << b << std::endl;
18
19    f();
20
21    std::cout << "after call f()" << std::endl;
22    std::cout << "a: " << &a << ", " << a << std::endl;
23    std::cout << "b: " << &b << ", " << b << std::endl;
24
25    return 0;
26}

运行结果如下:

1before call f()
2a: 0x7ffed44df7e8, 0
3b: 0x7ffed44df7ec, 0
4call f()
5a: 0x7ffed44df810, 1    # <--- 注意这里a的地址,说明参数a是值传递参数
6b: 0x7ffed44df7ec, 1    # <--- 注意这里b的地址,说明参数b是引用传递参数
7after call f()
8a: 0x7ffed44df7e8, 0
9b: 0x7ffed44df7ec, 1

由于std::bind()不知道生成的函数执行的时候传递的参数是否还有效,所以它选择按值传参而不是按引用传参。

通过这个例子,我们发现通过std::ref()对一个对象进行包装,使得值传递参数的函数调用,看起来像是传递了一个引用。

实际上,std::ref()只是尝试模拟引用,并不能真正变成引用,它本质上是返回了一个std::reference_wrapper类型的对象。std::ref()只有在模板类型推导或类型隐式转换时,才能生效,即实现引用传递。std::ref()能使用引用包装对象作为返回值,来代替原本会被识别的值类型,而std::reference_wrapper能隐式转换为被引用的值的引用类型。

auto r = std::ref(o);实际上等价于std::reference_wrapper<decltype(o)> r(o);

那么std::reference_wrapper又是怎么一回事呢?

什么是引用包装std::reference_wrapper

std::reference_wrapper是一个模板类类型,它将对象进行一层额外的包装,实现了这种语义:给我一个对象,返给你一个引用。除了我们上面std::bind()传值参数包装成std::reference_wrapper进行引用传递外,还可以用于STL容器元素的存储。

STL容器如vector,容器的元素可以是对象,也可以实对象的指针,唯独不能是对象的引用,也不能是引用的指针,况且C++标准规定,不允许有引用的指针。STL容器提供的是value语义而不是reference语义,所以容器不支持元素为引用,而用std::reference_wrapper可以实现。

我们来看下面这个例子:

 1std::list<int> l(10);
 2std::iota(l.begin(), l.end(), -4); //从-4开始填充序列
 3
 4//定义一个元素为引用包装类型的vector,用用数据来源为list
 5std::vector<std::reference_wrapper<int>> v(l.begin(), l.end());
 6
 7auto print = [&]() {
 8    std::cout << "list: ";
 9    for (int n : l) std::cout << n << ", ";
10    std::cout << std::endl;
11
12    std::cout << "vector: ";
13    for (int n : v) std::cout << n << ", ";
14    std::cout << std::endl;
15};
16
17std::cout << "Original: " << std::endl;
18print();
19
20std::cout << "Shuffling: " << std::endl;
21std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
22print();
23
24std::cout << "Doubling: " << std::endl;
25for (int& n : v) n *= 2;
26print();

运行结果为:

1Original:
2list: -4, -3, -2, -1, 0, 1, 2, 3, 4, 5,
3vector: -4, -3, -2, -1, 0, 1, 2, 3, 4, 5,
4Shuffling:
5list: -4, -3, -2, -1, 0, 1, 2, 3, 4, 5,
6vector: -3, 5, 3, 1, -2, -1, 2, -4, 0, 4,
7Doubling:
8list: -8, -6, -4, -2, 0, 2, 4, 6, 8, 10,
9vector: -6, 10, 6, 2, -4, -2, 4, -8, 0, 8,

注意vector并没有存储引用,而是存储的对象,即std::reference_wrapper对象。

std::reference_wrapper的意义在于

  • 引用不是对象,不存在引用的指针、引用的数组等,但引用包装使得定义引用的容器成为可能。

  • 模板函数无法辨别传入左值引用的意图是传值还是传引用,std::ref()std::cref()明确告诉模板函数是传引用。

std::reference_wrapper和std::ref的实现原理

std::reference_wrapper的简化版关键源码如下:

 1template<typename _Tp>
 2class reference_wrapper
 3{
 4    _Tp* _M_data;
 5
 6    constexpr static _Tp* _S_fun(_Tp& __r) noexcept { return std::__addressof(__r); }
 7
 8    static void _S_fun(_Tp&&) = delete;
 9
10    template<typename _Up, typename _Up2 = __remove_cvref_t<_Up>>
11    using __not_same = typename enable_if<!is_same<reference_wrapper, _Up2>::value>::type;
12
13public:
14    typedef _Tp type;
15
16    template<typename _Up, typename = __not_same<_Up>, typename
17        = decltype(reference_wrapper::_S_fun(std::declval<_Up>()))>
18    constexpr reference_wrapper(_Up&& __uref) noexcept(noexcept(reference_wrapper::_S_fun(std::declval<_Up>())))
19        : _M_data(reference_wrapper::_S_fun(std::forward<_Up>(__uref)))
20    { }
21
22    reference_wrapper(const reference_wrapper&) = default;
23
24    reference_wrapper& operator=(const reference_wrapper&) = default;
25
26    constexpr operator _Tp&() const noexcept { return this->get(); }
27
28    constexpr _Tp& get() const noexcept { return *_M_data; }
29
30    template<typename... _Args>
31    constexpr typename result_of<_Tp&(_Args&&...)>::type
32    operator()(_Args&&... __args) const
33    {
34        return std::__invoke(get(), std::forward<_Args>(__args)...);
35    }
36};

std::reference_wrapper也没有那么神秘,只不过包装了一个原始指针罢了。它重载了&操作符,对象可以隐式地转换回原来的引用。它重载了=操作符,包装仿函数时可以直接使用函数调用运算符。调用其他成员函数时,要先用get()函数获得其内部的引用。

std::refstd::cref的关键源码如下:

 1template<typename _Tp>
 2constexpr inline reference_wrapper<_Tp> ref(_Tp& __t) noexcept
 3{
 4    return reference_wrapper<_Tp>(__t);
 5}
 6
 7template<typename _Tp>
 8constexpr inline reference_wrapper<const _Tp> cref(const _Tp& __t) noexcept
 9{
10    return reference_wrapper<const _Tp>(__t);
11}
12
13template<typename _Tp>
14void ref(const _Tp&&) = delete;
15
16template<typename _Tp>
17void cref(const _Tp&&) = delete;
18
19template<typename _Tp>
20constexpr inline reference_wrapper<_Tp> ref(reference_wrapper<_Tp> __t) noexcept
21{
22    return __t;
23}
24
25template<typename _Tp>
26constexpr inline reference_wrapper<const _Tp> cref(reference_wrapper<_Tp> __t) noexcept
27{
28    return { __t.get() };
29}

std::refstd::cref更加简单,就是分别创建一个左值引用的引用包装对象和一个常量左值引用的引用包装对象返回。对于本身就是引用包装的对象,也支持使用std::refstd::cref,只不过原样返回。

Prev Post: 『C++移动语义、万能引用、引用折叠、完美转发』
Next Post: 『C++复制消除与RVO/NRVO』