本文测试环境:
系统:Linux ubuntu 4.2.0-16-generic #19-Ubuntu SMP x86_64 GNU/Linux
gcc版本: gcc version 5.2.1 20151010 (Ubuntu 5.2.1-22ubuntu2)
神奇的emplace_back函数
使用std::vector时,要么存储的是指针类型,要么是值类型。指针类型是指,我把一个对象放在别的地方,比如说堆内存,然后把这个对象的内存地址放在vector里;值类型是值,我不把对象放别的地方了,而是直接放到vector自己的内存空间里。
对于值类型的情况,要考虑一个问题:往vector插入对象,这个操作可能开销会很大。
比如看下面这段测试代码:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <vector>
using namespace std;
class Item {
public:
char name;
int val;
public:
~Item() {
printf("[dtor called] (%c, %i)\n", name, val);
}
Item() :name('_'), val(0) {
printf("[default ctor called] \n");
}
Item(char n, int v) :name(n), val(v) {
printf("[ctor called] (%c, %i)\n", name, val);
}
Item(Item&& a) {
printf("[move ctor called] (%c, %i)\n", a.name, a.val);
name = a.name;
val = a.val;
}
};
int main() {
vector<Item> v1;
for (int i = 0; i < 3; i++) {
v1.emplace_back('a', i);
}
printf("-----------------------\n");
vector<Item> v2;
v2.reserve(10);
for (int i = 0; i < 3; i++) {
v2.emplace_back('b', i);
}
printf("-----------------------\n");
vector<Item> v3;
v3.push_back({ 'c', 3 });
printf("-----------------------\n");
return 0;
}
编译:
gcc test.cpp -o test.out -std=gnu++11 -lstdc++
运行结果:
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
[ctor called] (a, 0)
[move ctor called] (a, 0)
[dtor called] (a, 0)
[ctor called] (a, 1)
[move ctor called] (a, 0)
[move ctor called] (a, 1)
[dtor called] (a, 0)
[dtor called] (a, 1)
[ctor called] (a, 2)
-----------------------
[ctor called] (b, 0)
[ctor called] (b, 1)
[ctor called] (b, 2)
-----------------------
[ctor called] (c, 3)
[move ctor called] (c, 3)
[dtor called] (c, 3)
-----------------------
[dtor called] (c, 3)
[dtor called] (b, 0)
[dtor called] (b, 1)
[dtor called] (b, 2)
[dtor called] (a, 0)
[dtor called] (a, 1)
[dtor called] (a, 2)
观察发现:
- 第一段测试,有多余的函数调用:move构造函数以及析构函数
- 第二段测试,没有多余的调用
- 第三段测试,有多余的函数调用:move构造函数以及析构函数
所以第二种写法是性能最好的。能够直接在vector的内存空间中构造对象。其他写法都会生成临时对象。
然而实际编程中,并不是总能这样子写,因为reverse的参数该填多少,需要细心考虑;如果vector存的是基类指针类型,那么上面任意一种写法差别都不大(最多拷贝一个指针地址而已)。
这些问题另当别论,现在回到本文主题上。
这个例子中的:v2.emplace_back('b', i),其实就是用完美转发实现的。
Imperfect forwarding
理解完美转发之前,先搞懂什么是不完美转发。下面会用一些测试代码来分析一下。
前置说明:
func是随便写的一个普通函数;wrapper是对func的一层封装;测试过程是控制变量法,针对特定的wrapper函数写法,不断修改func的参数的类型以及wrapper的调用方式,测试程序是否可以编译并且wrapper函数是否能够正确完成作为一个“封装函数”的基本要求。
第1组测试:func参数为 const int p
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
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
//used for switching the two test cases below
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // ok
func(c); // ok
func(d); // ok
func(1); // ok
func(number99()); // ok
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // ok
wrapper(c); // ok
wrapper(d); // ok
wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#endif
第1组测试,wrapper函数就和func表现得不一致了。
第2组测试:func参数为 int p
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
33
34
35
36
37
38
39
40
41
42
43
44
void func(int p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
//used for switching the two test cases below
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // ok
func(c); // ok
func(d); // ok
func(1); // ok
func(number99()); // ok
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // ok
wrapper(c); // ok
wrapper(d); // ok
wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#endif
结果和第1组一样。
第3组测试:func参数为 int& p
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
33
34
35
36
37
38
39
40
41
42
43
void func(int& p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
func(c); // ok
func(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
func(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
func(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
wrapper(c); // ok
wrapper(d); // error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers
wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#endif
这个情况,其实编译结果还是不一致的。若想知道具体细节请自己编译一遍。
第4组测试:func参数为 const int& p
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
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int& p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
//used for switching the two test cases below
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // ok
func(c); // ok
func(d); // ok
func(1); // ok
func(number99()); // ok
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // ok
wrapper(c); // ok
wrapper(d); // ok
wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#endif
结果和第1、2组一样。
小结
测试先到这里。由测试结果可以知道,这个wrapper是失败的(第1、2、4组测试,连最基本的编译结果都不一样)。
在c++11之前,对上面的不一致问题,是用非常暴力的方式的解决的,方式就是重载出N个wrapper的函数。
比如,把上面的第1组测试的代码改成:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void func(const int p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
/*--- a override of wrapper ---*/
template <typename T>
void wrapper(const T& p) { func(p); }
//used for switching the two test cases below
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // ok
func(c); // ok
func(d); // ok
func(1); // ok
func(number99()); // ok
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // ok
wrapper(c); // ok
wrapper(d); // ok
wrapper(1); // ok
wrapper(number99()); // ok
}
#endif
增加了这段代码:
cpp
template <typename T>
void wrapper(const T& p) { func(p); }
test_wrapper就编译通过了。(只需要注意编译结果的一致性,暂且忽略运行结果的一致性)
由此可以思考一下:如果func有N个参数,每个参数都要写const和非const两个版本,那么总共要写的wrapper函数就有2的n次方个!多么可怕。
reference deduction (collapsing)
引用推导(或引用折叠)规则,是c++11开始才有的一个说法,具体是怎么回事呢?请看下面的代码:
typedef int& lref;
typedef int&& rref;
int n = 100;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
(摘自http://en.cppreference.com/w/cpp/language/reference )
我用visual studio 2015跑了下这段代码:
看来是没错的。
总结了下这套推导规则:
- A& & -> A&
- A& && -> A&
- A&& & -> A&
- A&& && -> A&&
(记忆方法:只要有&,结果肯定是&)
C++中,有一对重要的兄弟:lvalue(左值)、rvalue(右值)。如何区分?简单来说就是,具名的是左值,不具名的是右值。
要注意一个事情:上面的4个变量r1、r2、r3、r4都是左值。即使r4的类型是右值引用,但因为r4是具名的,所以r4是左值。
Perfect forwarding
什么是完美转发?说白了就是要把上面那个不完美的wrapper,改造成完美的wrapper。
而且,改造过程只能在c++11以上版本才能实现。幸运的是,实现方式并不复杂,如下:
1
2
3
4
template <typename T>
void wrapper(T&& p) {
func(std::forward<T>(p));
}
再做这个新wrapperd的测试前,先把一些相关的函数介绍一遍。
remove_reference
vs2015给出的remove_reference实现是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class _Ty>
struct remove_reference
{ // remove reference
typedef _Ty type;
};
template<class _Ty>
struct remove_reference<_Ty&>
{ // remove reference
typedef _Ty type;
};
template<class _Ty>
struct remove_reference<_Ty&&>
{ // remove rvalue reference
typedef _Ty type;
};
这个东西,其实一目了然了,用3个重载保证remove_reference<类型>::type肯定没有&符号。后面的2个重载是必须的,当只定义了不带&符号的remove_reference时,remove_reference会没有效果。
其中比较诡异的是,后面的2个同名是不能单独存在的(会编译报错),必须先定义不带&符号的remove_reference,才能定义带&符号的remove_reference。(可以自己编译试试)
remove_reference在std::forward里会被使用。
forward函数
wrapper用到的std::forward,vs2015给出的实现是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TEMPLATE FUNCTION forward
template<class _Ty> inline
_CONST_FUN _Ty&& forward(
typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT
{ // forward an lvalue as either an lvalue or an rvalue
return (static_cast<_Ty&&>(_Arg));
}
template<class _Ty> inline
_CONST_FUN _Ty&& forward(
typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{ // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
return (static_cast<_Ty&&>(_Arg));
}
看着有点复杂,改造下(只保留关键代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
T&& Forward(
typename remove_reference<T>::type& p)
{
// 把一个左值转发成左值或右值
return (static_cast<T&&>(p));
}
template<class T>
T&& Forward(
typename remove_reference<T>::type&& p)
{
// 把一个右值转发成右值
return (static_cast<T&&>(p));
}
测试一下这个函数的运行情况:
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
33
34
35
36
37
38
39
40
41
//姑且称这个Forward为左值Forward
template<class T>
T&& Forward(
typename remove_reference<T>::type& p)
{
// 把一个左值转发成左值或右值
return (static_cast<T&&>(p));
}
//右值Forward
template<class T>
T&& Forward(
typename remove_reference<T>::type&& p)
{
// 把一个右值转发成右值
return (static_cast<T&&>(p));
}
int main() {
int a = 10;
int& b = a;
int&& c = 10;
Forward<int>(a);
Forward<int&>(a);
Forward<int&&>(a);
Forward<int>(b);
Forward<int&>(b);
Forward<int&&>(b);
Forward<int>(c);
Forward<int&>(c);
Forward<int&&>(c);
Forward<int>(10);
Forward<int&>(10);
Forward<int&&>(10);
return 0;
}
断点调试发现:
- 以左值(a、b、c具名,所以是左值)作为参数去调用三个实例化模板函数,进入的都是左值Forward
- 以右值(10不具名,所以是右值)作为参数去调用三个实例化模板函数,进入的都是右值Forward
- 在左值Forward函数体内,p的类型是int&
- 在右值Forward函数体内,p的类型是int&&
左值Forward的推导过程
Forward(a),T是int,所以:
int && Forward(typename remove_reference<int>::type& p)
{
return (static_cast<int &&>(p));
}
最终变成:
int & Forward(int& p)
{
return (static_cast<int &>(p));
}
Forward(a),T是int&,所以:
int & && Forward(typename remove_reference<int &>::type& p)
{
return (static_cast<int & &&>(p));
}
根据上文说的引用推导规则,这个函数会变成:
int & Forward(int& p)
{
return (static_cast<int &>(p));
}
Forward(a),T是int&&,所以:
int && && Forward(typename remove_reference<int &&>::type& p)
{
return (static_cast<int && &&>(p));
}
根据上文说的引用推导规则,这个函数会变成:
int && Forward(int& p)
{
return (static_cast<int &&>(p));
}
小结:
- 当Forward的参数是左值时,调用的是左值Forward版本
- 当Forward的‘模板类型’是int或int&时,Forward实例化成:
int & Forward(int& p)
{
return (static_cast<int &>(p));
}
- 当Forward的‘模板类型’是int&&时,Forward实例化成:
int && Forward(int& p)
{
return (static_cast<int &&>(p));
}
也就是说,
- 当参数是左值时,它必然是以int&(即左值引用)的形式进入到Forward;
- 当Forward模板类型是int或int&时,返回值类型必然是int&;
- 当Forward模板类型是int&&时,返回值类型必然是int&&。
右值Forward的推导过程
Forward(10),T是int,所以:
int && Forward(typename remove_reference<int>::type&& p)
{
return (static_cast<int &&>(p));
}
最终变成:
int && Forward(int && p)
{
return (static_cast<int &&>(p));
}
Forward(10),T是int&,所以:
int & && Forward(typename remove_reference<int &>::type&& p)
{
return (static_cast<int & &&>(p));
}
根据上文说的引用推导规则,这个函数会变成:
int & Forward(int && p)
{
return (static_cast<int &>(p));
}
Forward(10),T是int&&,所以:
int && && Forward(typename remove_reference<int &&>::type&& p)
{
return (static_cast<int && &&>(p));
}
根据上文说的引用推导规则,这个函数会变成:
int && Forward(int && p)
{
return (static_cast<int &&>(p));
}
小结:
- 当Forward的参数是右值时,调用的是右值Forward版本
- 当Forward的‘模板类型’是int或int&&时,Forward实例化成:
int && Forward(int && p)
{
return (static_cast<int &&>(p));
}
- 当Forward的‘模板类型’是int&时,Forward实例化成:
int & Forward(int && p)
{
return (static_cast<int &>(p));
}
- 也就是说,当参数是右值时,它必然是以int&&(即右值引用)的形式进入到Forward;
- 当Forward模板类型是int或int&&时,返回值类型必然是int&&;
- 当Forward模板类型是int&时,返回值类型必然是int&。
universal references
对完美wrapper的另一个部分做分析:
1
2
3
template <typename T>
void wrapper(T&& p) {
}
这个wrapper有什么效果?测试下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
void wrapper(T&& p) {
}
int main() {
int a = 10;
int& b = a;
int&& c = 10;
wrapper(a);
wrapper(b);
wrapper(c);
wrapper(10);
return 0;
}
用vs2015断点进入wrapper函数,发现:
- wrapper(a),p的类型是int&
- wrapper(b),p的类型是int&
- wrapper(c),p的类型是int&
- wrapper(10),p的类型是int&&
这个规则有点不直观。wrapper的模板类型是T,参数是T&&,为什么传一个左值int a进去,不是得到int && p,而是int & p?
这是因为,在这个wrapper中,T&& p并不是单纯的右值引用,而是叫universal references。(泛引用?)
在Scott Meyers的这篇文章Universal References in C++11—Scott Meyers中,Scott Meyers做了如下定义:
If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.
翻译一下:
如果一个变量或参数被声明为推导类型T对应的T&&类型,那么这个变量或参数是一个universal reference。
回到wrapper函数
1
2
3
4
template <typename T>
void wrapper(T&& p) {
func(std::forward<T>(p));
}
根据之前的测试,可以知道:
(为了解释方便,以int来说明)
- 当传递给wrapper的实参是左值时,T&& p变成 int& p
- 当传递给wrapper的实参是右值时,T&& p变成 int&& p
- wrapper的forward是左值forward (因p具名,p是左值)
- p是以int&的形式进入到forward (p是左值)
- 当forward模板类型是int或int&时,forward返回值类型必然是int&
- 当forward模板类型是int&&时,forward返回值类型必然是int&&
- 根据1、2、5、6可以得出8、9
- 当传递给wrapper的实参是左值时,T&& p变成 int& p,forward返回值类型必然是int&
- 当传递给wrapper的实参是右值时,T&& p变成 int&& p,forward返回值类型必然是int&&
所谓的完美转发,指的就是第8、9这两个性质。
调用层给wrapper任意类型(int a、int& a、int&& a)的左值,wrapper都以int&转发给func; 调用层给wrapper任意右值(不具名常量、函数返回值等),wrapper都以int&&转发给func。
最后,再来看下一开始的不完美wrapper的问题:
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
33
34
35
36
37
38
39
40
41
42
43
44
void func(const int p) {
}
int number99(){
return 99;
}
template <typename T>
void wrapper(T& p) { func(p); }
//used for switching the two test cases below
#define TEST_FUNC
#if defined(TEST_FUNC)
void test_func(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
func(a); // ok
func(b); // ok
func(c); // ok
func(d); // ok
func(1); // ok
func(number99()); // ok
}
#else
void test_wrapper(){
int a = 1;
const int b = 1;
int& c = a;
const int& d = a;
wrapper(a); // ok
wrapper(b); // ok
wrapper(c); // ok
wrapper(d); // ok
wrapper(1); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
wrapper(number99()); // error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
}
#endif
wrapper(1) 和 wrapper(number99()) 这2个为何报错,读者现在应该明白了。
好吧我还是把话说完吧:
wrapper的T& p遇到任何引用类型的T,都只会变成int& p。因为引用折叠规则(见上文)就是这样子规定。而又因为1和number99()返回值,都是右值,那么传递进wrapper时,就是int& p = 1, int& p = number99(),显然这会编译错误。
本文结束。
参考资料
http://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c/
写作不易,您的支持是我写作的动力!