技术: C++11 std::ref 总结

Boost 以及 标准库 中的 ref库, 谈谈该库的来龙去脉, 使用技巧.

智能引用, 一种对于cpp现存机制的引用不足的弥补.
C++11引入 std::reference_wrapper 和 boost中谈到的稍稍有些区别, 应该说比 boost 中的更加强大了.

引子

智能引用 ref 或者 cref 或者 reference_wrapper 有如下三个作用:

  • 包装你给的对象引用, 其实就是引用的包装提
  • 传参数时, 消除复杂对象的拷贝代价
  • 将不可拷贝对象转换为可拷贝对象(noncopyable, singleton之类的)

本文涉及到如下类模板:

  • std::reference_wrapper
  • std::ref
  • std::cref

完整定义如下:

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
# include <functional>

/*------------reference_wrapper*/
template< class T >
class reference_wrapper;


/*------------------ref*/
template< class T >
std::reference_wrapper<T> ref(T& t) noexcept;

template< class T >
std::reference_wrapper<T> ref( std::reference_wrapper<T> t ) noexcept;

//注意不允许右值构造器
template <class T>
void ref(const T&&) = delete;

/*-----------------cref*/
template< class T >
std::reference_wrapper<const T> cref( const T& t ) noexcept;

template< class T >
std::reference_wrapper<const T> cref( std::reference_wrapper<T> t ) noexcept;

template <class T>
void cref(const T&&) = delete;

正文

如果某个调用, 需要一个对象(passed-by-value)作为参数,
比如说容器里面只能存入对象, 对象的指针, 而不能存入引用, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <numeric>


using namespace std;



int main(void)
{
vector<int> v(10);
iota(v.begin(), v.end(), 0);
/*v : 0 1 2 3 4 5 6 7 8 9*/

vector<int&> v2;


return 0;
}

不用多看, 报错了, 因为vecotr容器内部机制涉及迭代器, 迭代器保留了原容器的指针或者index, 总之容器内部涉及指针操作, 而c++标准规定了, 不允许有引用的指针.

如果编译报错, 那么只能存储对象.

1
vecotr<reference_wrapper<int>> v2;

上面的 reference_wrapper 其实是模板类类型, 也就是int类型元素的引用类型; 此时解决了上述问题:

  • 容器里面没有存储引用, 而是存储的对象(reference_wrapper对象)
  • 并且 reference_wrapper 是代表 int 元素的包装类

那么 “给你一个对象, 你返回一个引用” , 这个包装类怎么写呢? 参考Boost的实现, 可以很简单的给出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//引用类型的包装类
template<class T>
class reference_wrapper
{
private:
T *t_;

public:

//把对象的地址拿来初始化内部维护的指针std::addressof(t)
explicit refrence_wrapper(T &t): t_(&t) {}

// 隐式转换
operator T&() const { return *t_;} //这个是在原对象的位置传入wrapper对象的基础

T& get() const { return *t_; } //拿到引用对象的真实引用(方便调用原对象的方法), 而不是本包装类对象
T* get_ptr() const { return t_;} //拿到内部维护的指针
}

这样, 在使用智能引用就可以放入容器了, 并且, 原来参数需要原始对象的地方, 由于隐式转换的存在, 也可以调用了.

当然标准库的可能实现会更加安全(阉割了一部分功能), 并且多了 operator()() 的支持:

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

template <class T>
class reference_wrapper {
public:
// types
typedef T type;

// construct/copy/destroy
reference_wrapper(T& ref) noexcept : _ptr(std::addressof(ref)) {}
reference_wrapper(T&&) = delete;
reference_wrapper(const reference_wrapper&) noexcept = default;

// assignment
reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;

// access
operator T& () const noexcept { return *_ptr; }
T& get() const noexcept { return *_ptr; }

template< class... ArgTypes >
typename std::result_of<T&(ArgTypes&&...)>::type
operator() ( ArgTypes&&... args ) const {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}

private:
T* _ptr;
};

注意, 这里没有提供 get_ptr() 方法, 也就是不希望你去使用 内部指针. 当然, 简单使用起来就是不言自明的:

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
#include <iostream>
#include <cassert>
#include <functional>

using namespace std;

template<typename T>
using wrap = reference_wrapper<T>;

int main(void)
{
int x = 1;

wrap<int> wx(x);

assert( x == wx); //隐式类型转换

wrap<int> wx2(wx); //拷贝构造
assert( wx.get() == wx2);

string str;
wrap<string> ws(str);
ws.get() = "hello world"; //赋值给其真实引用

cout << ((string&)ws).size() << endl;//11

return 0;
}

如果你真的不嫌弃麻烦每次都生成reference_wrapper的对象的话; 其实你可以直接使用它的工厂方法ref, cref生成对象, 直接用:

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
31
32
//std::reference_wrapper<T> ref(T& t) noexcept;
double x = 2.0;
std::cout << std::sqrt(std::ref(x)) << std::endl; //直接用在需要拷贝语义的sqrt算法函数上
std::cout << std::sqrt(std::cref(x)) << std::endl; //cref用于不修改原值状态的引用

//如果是函数对象的包装引用, 可以直接调用
struct square
{
void operator()(int &x) {
x = x * x;
}

};

int x = 5;
square sq;

//std::ref(sq)(x);
std::reference_wrapper<struct square> wsq = std::ref(sq);
wsq(x);

std::cout << x << std::endl;

//std::cout << std::ref(square)(5) << std::endl; //函数调用返回void

//如果是普通函数的话, 就非常简单了
void f1()
{
std::cout << "reference to function called\n";
}
std::reference_wrapper<void()> ref1 = std::ref(f1);
ref1();

但是很可惜, Boost中的 is_reference_wrapper<T>::value 判断是否为包装类型对象, 以及 unwrap_reference<T>::type 判断其真实类型(无论是否包装), 在标准库都用不了. 更不要说解包装 unwrap_ref() 了.(解包装调用原来对象的具体方法, 请用get()方法吧)

尾巴

总而言之, 这样一种智能引用机制, 在你原来不能使用引用的地方, 用包装的方式, 实现了另外一个可能, 方便你的调用.

参考资料

  1. boost手册
  2. http://en.cppreference.com/w/cpp/utility/functional/reference_wrapper
文章目录
  1. 1. 引子
  2. 2. 正文
  3. 3. 尾巴
  4. 4. 参考资料
|