10C++11通用为本,专用为末_1
1. 继承构造函数
struct A
{
A(int i){}
A(double d, int i){}
A(float f, int i, const char* c){}
};
struct B : public A
{
using A::A; //继承构造函数
virtual void extraInterface(){}
}
这里我们通过 using A::A 的声明,把基类中的构造函数悉数集成到派生类 B 中。这样就不需要要再为派生类定义多个构造函数了。
2. 委派构造函数(主要用于多个重载构造函数中)
class Info
{
public:
Info(){initRest()};
Info(int i):type(i) {initRest();}
Info()(char e):name(e) {initRest();}
private:
void initRest(){}
int type {1};
char name {'a'};
}
上例中,每个构造函数都需要调用 initRest 函数进行初始化。而现实编程中,构造函数中的代码还会更长,比如可能还需要一些基类的构造函数等。那能不能在一些构造函数中连 initRest 都不用调用呢?
在 c++11 中,我们可以使用委派构造函数来达到预期的效果。
class Info
{
public:
Info(){initRest()};
Info(int i):Info() { type = i;}
Info()(char e):Info() {name = e;};
private:
void initRest(){}
int type {1};
char name {'a'};
}
上例中,我们在 Info(int) 和 Info(char) 的初始化列表的位置,调用了“基准版本”的构造函数 Info()。这里我们为了区分被调用者和调用者,称在初始化列表中调用“基准版本”的构造函数为 委派构造函数(delegating construnctor),而被调用的“基准版本”则为目标构造函数(target constructor)。
在 C++11 中,所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。
注意:在 C++ 中,构造函数不能同时 “委派”和使用初始化列表。
3. 移动语义
拷贝构造函数中未指针成员分配新的内存再进行内容拷贝的做法在 c++ 编程中几乎被视为不可违背的。不过有些时候,我们确实不需要这样的拷贝构造语义,这时候就可以使用 C++ 提供的移动语义。
#include <iostream>
using namespace std;
class HasPtrMem
{
public:
HasPtrMem() : d(new int (3))
{
cout << "Construct:" << ++ n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h): d(new int(*h.d))
{
cout << "Copy construct:" << ++n_cptr <<endl;
}
//移动构造函数
HasPtrMem(HasPtrMem && h): d(h.d)
{
h.d = nullptr; //将临时值的指针成员置空
cout << "Move construct:" << ++n_mvtr <<endl;
}
~HasPtrMem()
{
delete d;
cout << "Destruct:" << ++n_dstr <<endl;
}
int * d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
HasPtrMem getTemp()
{
HasPtrMem h;
cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
return h;
}
int main()
{
HasPtrMem a = getTemp();
cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}
上例中,HasPtrMem( HasPtrMem &&) 就是所谓的移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的 “右值引用” 的参数。可以看到,移动构造函数使用了参数 h 的成员 d 初始化了本对象的成员 d (而不是像拷贝构造函数一样需要分配内存,然后将内容依次拷贝到新分配的内存中),而 h 的成员 d 随后被置为指针空值 nullptr。这就完成了移动构造的全过程。
这里所谓的 “偷” 堆内存,就是指将本对象 d 指向 h.d 所指的内存这一条语句,相应地,我们还将 h 的成员 d 置为指针空值。这其实也是我们 “偷” 内存时必须做的。这是因为在移动构造完成之后,临时对象会立即被析构。如果不改变 h.d (临时对象的指针成员)的话,则临时对象会析构掉本是我们 “偷” 来的堆内存。这样一来,本对象中的 d 指针也就成了一个悬挂指针,如果我们对指针进行解引用,就会发生严重的运行时错误。
那么还有一个最为关键的问题没有解决,那就是移动构造函数何时会被触发。之前我们只是提到了临时对象,一旦我们用到的是个临时变量,那么移动构造函数就可以得到执行。
那么,在 C++ 中如何判断产生了临时对象?如何将其用于移动构造函数?是否只有临时变量可以用于移动构造?
4. 左值、右值与右值引用
4.1 左值与右值
左值、右值的最为典型的判断方法就是,在赋值表达式中,出现在等号左边的就是 “左值”,而在等号右边的,则称为 “右值”。
另一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的,没有名字的就是右值。
在 c++11 中,右值是由两个概念构成的,一个是将亡值(xvalue),另一个则是纯右值(prvalue)。
其中纯右值就是 c++98 标准中右值的概念。比如非引用返回的函数返回的临时变量值就是一个纯右值。一些运算表达式,比如 1+3 产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如 2、'c'、true 也是纯右值。此外,类型转换函数的返回值、lambda 表达式等,也都是右值。
而将亡值则是 c++11 新增的跟右值引用相关的表达式,这种表达式通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std::move 的返回值,或者转换为 T&& 的类型转换函数的返回值。
而剩余的,可以标识函数、对象的值都属于左值。 在 c++ 程序中,所有的值必属于左值、将亡值、纯右值三者之一。
4.2 右值引用
在 c++11 中,右值引用就是对一个右值进行引用的类型。事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:
T && a = returnRvalue();
//这个表达式中,假设 returnRvalue 返回一个右值,我们就声明可一个名为 a 的右值引用,其值等于 returnRvalue 函数返回的临时变量的值。
为了区别与 c++98 中的引用类型,我们称 c++98 中的引用为 “左值引用”。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是改对象的一个别名。最值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
在上面的例子中,returnRvalue(); 函数返回的右值在表达式语句结束后,其生命也就终结了,而通过右值又 “重获新生”,其生命期将与右值引用类型比变量 a 的生命期一样。只要 a 还 “活着”, 该右值临时量将会一直 “存活”下去。
T b = returnRvalue();
相比以上语句的声明方式,我们刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。因为 a 是右值引用,直接绑定了 ReturnRvalue() 返回的临时量。而 b 只是由临时值构造而成的,临时值在表达式结束后会析构,因此就会多一次析构和构造的开销。
4.3 常量左值引用
相对的,在 c++98 标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)呢?例如:
T & e = returnRvalue(); //编译出错
const T & f = returnRvalue(); //通过编译
//出现这样的状况的原因是,常量左值引用就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化
//而且在使用右值对其进行初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。
//不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。
//相对地,非常量左值只能接受非常量左值对其进行初始化。
在 c++11 之前,左值、右值对程序员来说,一致呈透明状态。不知道什么是左值、右值,并不影响写出正确的 c++ 代码。引用的是左值和右值通常也并不重要。
为了语义的完整, c++11 中还存在着常量右值引用,比如我们通过以下代码声明一个常量右值引用、
const T & crvalueref = returnRvalue();
//但是,一来右值引用主要就是为了移动语义,而移动语义需要右值是可以被修改的,那么常量右值引用在移动语义中就没有用武之处了。
//而来如果要引用右值且让常量右值引用不可以更改,常量左值引用往往就足够了。因此,目前我们还没有看到常量右值引用有何用处。
4.4 小结
有时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(这在模板中比较常见),标准库在 <type_traits> 头文件中提供了 3 个模板类: is_rvalue_reference、is_lvalue_reference、is_reference,可供我们进行判断。
cout << is_rvalue_reference<string &&>::value;
我们通过模板类的成员 value 就可以打印出 string&& 是否是一个右值引用了。配合第四章中的类型推导操作符 decltype, 我们甚至可以对变量的类型进行判断。
5. std::move 强制转化为右值
c++11 提供了一个函数 std::move, 这个函数名字具有迷惑性,因为实际上 std::move 并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,鸡儿我们可以通过右值引用使用该值,以用于移动语义。
从实现上讲,std::move 基本等同于一个类型转换:
static_cast<T&&>(lvalue);
基本原则就是,将一个左值使用 std::move 前置转换成右值引用之后,该左值就不能再使用了。
事实上,为了保证移动语义的传递,程序员在编写移动构造函数的时候,应该总是记得使用 std::move 转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大问题。
6. 移动语义的一些其他问题
6.1 实现移动语义一定要注意排除不必要的 cosnt 关键字
//移动语义一定是要修改临时变量的值
Moveable(const Moveable &&);
const Moveable returnVal();
//声明上述两种形式的语句,都会使临时变量常量化,成为一个常量右值,那么临时变量的引用也就我发修改了,从而导致无法实现移动语义。
6.2 如果需要移动语义,程序员必须自定义移动构造函数
默认情况下,编译器会为程序员隐式地生成一个(隐式表示如果不被使用则不生成)移动构造函数。不过如果程序员声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或者多个,编译器都不会再为程序员生成默认版本。
通常情况下,如果需要移动语义,程序员必须自定义移动构造函数。当然,对一些简单的,不包含任何资源的类型来说,实现移动语义与否都无关紧要,因为对这样的类型而言,移动就是拷贝,拷贝就是移动。
那对于包含容器的类呢?——配合 std::move 可以方便的实现移动构造函数。
#include <iostream>
#include <vector>
using namespace std;
class HasPtrMem
{
public:
HasPtrMem() : d(new int(3)), v(100, 1)
{
cout << "Construct:" << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) : d(new int(*h.d))
{
v = h.v;
cout << "Copy construct:" << ++n_cptr << endl;
}
//移动构造函数
HasPtrMem(HasPtrMem && h) : d(h.d)
{
v = std::move(h.v);
h.d = nullptr; //将临时值的指针成员置空
cout << "Move construct:" << ++n_mvtr << endl;
}
~HasPtrMem()
{
delete d;
cout << "Destruct:" << ++n_dstr << endl;
}
int * d;
std::vector<int> v;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
HasPtrMem getTemp()
{
HasPtrMem h;
cout << "d ptr " << __func__ << ": " << hex << h.d << endl; //d ptr getTemp: 008285A0
cout << "v ptr " << __func__ << ": " << hex << &(h.v) << endl; //v ptr getTemp: 004FF86C
cout << "v[0] ptr " << __func__ << ": " << hex << &(h.v[0]) << endl; //v[0] ptr getTemp: 008382A8
return h;
}
int main()
{
HasPtrMem a = getTemp();
cout << "d ptr " << __func__ << ": " << hex << a.d << endl; //d ptr main: 008285A0
cout << "v ptr " << __func__ << ": " << hex << &(a.v) << endl; //v ptr main: 004FFA4C
cout << "v[0] ptr " << __func__ << ": " << hex << &(a.v[0]) << endl; //v[0] ptr main: 008382A8
HasPtrMem a1(std::move(a));
cout << "d ptr " << __func__ << ": " << hex << a1.d << endl; //d ptr main: 008285A0
cout << "v ptr " << __func__ << ": " << hex << &(a1.v) << endl; //v ptr main: 004FFA30
cout << "v[0] ptr " << __func__ << ": " << hex << &(a1.v[0]) << endl; //v[0] ptr main: 008382A8
//std::string 不支持移动构造
std::string name = "hello world!";
printf("%s, &t:%p\n", name.c_str(), &name); //hello world!, &t:008FF8BC
printf("%s, &(t[0]):%p\n", name.c_str(), &(name[0])); //hello world!, &(t[0]):008FF8C0
std::string name_move(std::move(name));
printf("%s, &t:%p\n", name_move.c_str(), &name_move); //hello world!, &t:008FF898
printf("%s, &(t[0]):%p\n\n\n", name_move.c_str(), &(name_move[0])); //hello world!, &(t[0]):008FF89C
//std::vector 支持移动构造
std::vector<int> vNum = { 1,2,3,4,5 };
printf("vNum, &t:%p\n",&vNum); //vNum, &t:008FF880
printf("vNum, &(t[0]):%p\n", &(vNum[0])); //vNum, &(t[0]):000B4800
std::vector<int> vNumMove(std::move(vNum));
printf("vNumMove, &t:%p\n", &vNumMove); //vNumMove, &t:008FF868
printf("vNumMove, &(t[0]):%p\n", &(vNumMove[0])); //vNumMove, &(t[0]):000B4800
}
同样地,声明了移动构造函数、移动赋值函数、拷贝赋值函数和析构函数中的一个或者多个,编译器也不会再为程序员生成默认的拷贝构造函数。所以在 c++11 中,拷贝构造/赋值 和 移动构造/赋值函数必须同时提供,或者同时不提供,程序员才能保证类同时具有拷贝和移动语义。只声明其中一种的话,类都仅能实现一种语义。
7. 完美转发
所谓完美转发(perfect forwarding) 是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
#include <iostream>
using namespace std;
void runCode(int && m){cout << "rvalue ref" << endl;}
void runCode(int & m){cout << "lvalue ref" << endl;}
void runCode(const int && m){cout << "const rvalue ref" << endl;}
void runCode(cosntint & m){cout << "const lvalue ref" << endl;}
template<typename T>
void pefectForward(T && t) {runCode(forward<T>(t));}
int main()
{
int a, b;
const int c = 1;
const int d = 0;
pefectForward(a); //lvalue ref
pefectForward(move(b)); //rvalue ref
pefectForward(c); //const lvalue ref
pefectForward(move(d)); //const rvalue ref
return 0;
}
完美转发的一个作用就是做包装函数,这是一个很方便的例子。再看下面的例子。
#include <iostream>
using namespace std;
template <typename T, typename U>
void perfectForward(T &&t, U& func)
{
cout << t << \tforwarded... << endl;
func(forward<T>(t));
}
void runCode(double && m){};
void runHome(double && h){};
void runComp(double && c){};
int main()
{
perfectForward(1.5, runComp); //1.5 forwarded...
perfectForward(8, runCode); //8 forwarded...
perfectForward(1.5, runHome); //1.5 forwarded...
return 0;
}