2 通用为本,专用为末
2.1 继承构造函数
C++11中子类可以通过using声明来声明继承基类的构造函数。
struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
};
struct B: A {
using A::A; //继承构造函数
//等价于
//B(int i): A(i) {}
//B(double d, int i): A(d, i) {}
//B(float f, int i, const char* c): A(f, i, c) {}
};
struct A1 {A1(int) {} };
struct B1 {B1(int) {} };
struct C: A1, B1 {
using A1::A1;
using B1::B1;
C(int) {} //需要显示声明继承类中的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。
};
注意:
- 继承构造函数被设计为跟派生类中的各种类默认函数(默认构造、析构、拷贝构造等)一样,是隐式声明的。这意味着,如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。
- 继承构造函数只会初始化基类中成员函数,对于派生类中的成员变量,需要使用成员变量初始化来解决
- 基类构造函数的参数如果有默认值,对于继承构造函数来说,参数默认值是不会被继承的
2.2 委派构造函数
委派构造函数:在初始化列表中调用“基准版本”的构造函数为委派函数
目标构造函数:被调用的“基准版本”则为目标构造函数
注意:不能在初始化列表中既初始化列表中既初始化成员,又委托其构造函数完成构造。
注意:不能形成委托环。
2.3 右值引用:移动语义和完美转发
class HasPtrMem {
public:
HasPtrMem(HasPtrMem &&h): d(h.d) { //移动构造函数
h.d = nullptr;
}
int *d;
};
可以取地址的、有名字的就是左值,反之,不能取地址的,没名字的就是右值。C++11中,右值由两个概念构成,一个是将亡值(右值引用相关的表达式,如右值引用T&&的函数返回值,std::move的返回值),另一个是纯右值。
注意:右值引用主要为了移动语义,而移动语义需要右值是可以被修改的,所以常量右值引用在移动语义中就没有用武之地。
C++11中引用类型及其可以引用的值类型如下表:
引用类型 | 非常量左值 | 常量左值 | 非常量右值 | 常量右值 | 注记 |
---|---|---|---|---|---|
非常量左值引用 | Y | N | N | N | 无 |
常量左值引用 | Y | Y | Y | Y | 全能类型,可用于拷贝语义 |
非常量右值引用 | N | N | Y | N | 用于移动语义,完美转发 |
常量右值引用 | N | N | Y | Y | 暂无用处 |
2.3.1 std::move 强制转化为右值
//std::move等同与下面转化,唯一的功能是将一个左值强制转化为右值引用,从而可以通过右值引用使用该值,用于移动语义
static_cast<T&&>(lvalue);
//如果Copyable没有移动构造函数,那么将调用以常量左值引用为参数的拷贝构造函数。
Copyable news = std::move(s);
2.3.2 移动语义的一些其他问题
- 如果声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或者多个,编译器都不会再为程序员生成默认版本。
- 如果声明了移动构造函数、移动赋值函数、拷贝赋值函数和析构函数中的一个或者多个,编译器也不会再为程序员生成默认的拷贝构造函数。
所以C++11中,拷贝构造/赋值和移动构造/赋值函数必须同时提供,或者同时不提供,程序员才能保证类同时具有拷贝和移动语义。
RVO(return value optimization)或者NRVO(Named Return Value optimiation)称为返回值优化。
关闭返回值优化
-fno-elide-constructors
2.3.3 完美转发
完美转发:在函数模版中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
//参数在传给IrunCodeActually之前就产生一次额外的临时对象拷贝,谈不上完美。
template <typename T>
void IamForwarding(T t) {
IrunCodeActually(t);
}
C++11中的引用折叠规则
typedef const int T;
typedef T& TR;
TR& v = 1;
TR的类型定义 | 声明v的类型 | v的实际类型 |
---|---|---|
T& | TR | A& |
T& | TR& | A& |
T& | TR&& | A& |
T&& | TR | A&& |
T&& | TR& | A& |
T&& | TR&& | A&& |
规则:一旦模版类型为T&,那么v的实际类型就是A&, 模版的类型为T&&, 实际类型为模版类型的&&加上声明v的类型&的个数(减去&&)( 定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用)。
void IamForwording(T &&t) {
IrunCodeActually(static_cast<T&&>(t));
}
等价于:
void IamForwording(T &&t) {
IrunCodeActually(forward(t));
}
void IamForwording(X& &&t) {
IrunCodeActually(static_cast<X& &&>(t));
}
引用折叠引用为,等价于:
void IamForwording(X&t) {
IrunCodeActually(static_cast<X&>(t));
}
void IamForwording(X&& &&t) {
IrunCodeActually(static_cast<X&& &&>(t));
}
引用折叠引用为,等价于:
void IamForwording(X&& t) {
IrunCodeActually(static_cast<X&&>(t));
}
2.4 显示类型操作符
C++11中将explicit的使用范围扩展到了自定义的类型转换操作符上。
class ConvertTo{};
class Convertable {
public:
explict operator ConvertTo() const {
return ConvertTo();
}
};
void Func(ConvertTo ct) {}
void test() {
Convertable c;
ConvertTo ct(c); //直接初始化构造
ConvertTo ct2 = c; //拷贝构造初始化失败,编译失败
ConvertTo ct3 = static_cast<ConvertTo>(c); //强制转化,通过
Func(c); //拷贝构造初始化失败,编译失败
}
2.5 列表初始化
C++11 支持{}花括号的初始化形式,列表初始化化使唯一一种可以防止类型收窄的初始化方式。
int a[] = {1, 3, 5};
int b[] = {2, 4, 6};
vector<int> c{1, 3, 5};
map<int, float> d = {{1, 1.0f}, {2, 2.0f}};
double *d = new double{1.2f};
只需要#include <initializer_list>,并声明为一个以initializer_list模版类为参数的构造函数,就可以使自定义的类使用列表初始化。
enum Gender {boy, girl};
class People {
public:
People(initializer_list<pair<string, Gender>> lst) {
auto i = lst.begin();
for (; i != lst.end(); ++lst) {
data.push_back(*i);
}
}
private:
vector<pair<string, Gender> > data;
};
People ship = {{"Garfield", boy}, {"HelloKitty", girl}};
2.6 POD类型
C++11将POD划分为两个基本概念集合:平凡的和标准布局的。
2.6.1 平凡的类或结构体定义:
- 拥有平凡的默认构造函数和析构函数(即不定义类的构造函数,编译器自动生成一个平凡的默认构造函数)
- 拥有平凡的拷贝构造函数(基本等同于使用memcpy进行类型的构造)和移动构造函数
- 拥有平凡的拷贝赋值运算符和移动赋值运算符
- 不能包含虚函数以及虚基类
//使用类模版来帮助判断
template <typename T> struct std::is_trivial;
2.6.2 标准布局
- 所有非静态成员拥有相同的访问权限
//a和b是不同的权限,所以该匿名结构体不是标准布局
struct {
public:
int a;
private:
int b;
};
- 在类或者结构体继承时,满足以下两种情况之一:
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类
- 基类有非静态成员,而派生类没有非静态成员
非静态成员只要同时出现在派生类和基类中,即不属于标准布局
因此一旦非静态成员出现在多个基类中,派生类也不属于标准布局
- 类中第一个非静态成员的类型与其基类不同
C++标准中,如果基类没有成员,标准允许派生类的第一个成员与基类共享地址。但是如果基类的第一个成员仍然是基类类型,编译器仍会为基类分配1字节的空间,因为C++标准要求类型相同的对象必须地址不同(基类地址与派生类中的成员b的地址必须不同)
//A不是一个标准布局的类型,因为第一个非静态成员变量b的类型跟A所继承的类型B相同。
struct A : B { B b;}
- 没有虚函数和虚基类
- 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局。
//使用类模版来帮助判断
template <typename T> struct std::is_standard_layout;
POD类型的好处:
- 字符赋值,代码中使用memset和memcpy对POD类型进行初始化和拷贝等操作
- 提供对C内存布局兼容。C++程序可以和C函数进行相互操作,因为POD类型的数据在C和C++间的操作总是安全的。
- 保证了静态初始化的安全有效。静态初始化在很多时候能提高程序的性能,而POD类型的对象初始化更简单(比如放入目标文件的.bss段,在初始化中直接被赋0)
2.7 非受限联合体
C++ 98中,联合体内不允许有POD类型成员,不允许有静态或引用类型的成员。C++11中,任何非引用类型都可以成为联合体的数据成员。(实际可能不允许静态成员变量的存在,否则所有该类型的联合体将共享一个值)。
在C++ 11中,标准会默认删除一些非受限联合体的默认函数,如,非受限union有一个非POD成员,而该成员类型有非平凡的构造函数,那么非受限union的默认构造函数将被编译器删除。默认copy函数,copy赋值操作符以及析构函数等,也遵循此规则。
union myUnion
{
string s;
int i{1};
};
myUnion t; //compile error,因为myUnion的默认构造函数被删除了
解决办法,自己为非受限union定义构造函数
union myUnion
{
public:
myUnion()
{
new (&s) string("1233"); //placement new
}
~myUnion()
{
s.~string(); //析构
}
string s;
int i{1};
};
2.8 内联名字空间
C++ 98不允许在不同的名字空间中对模板进行特化。
C++ 11中引入 inline namespace 內联名字空间,允许在父名字空间定义或特化子名字空间的模板。
#include <iostream>
using namespace std;
namespace Jim {
inline namespace Basic {
struct Knife{};
class CorkScrew{};
};
inline namespace ToolKit {
template<typename T> class SwissArmyKnife{};
}
namespace Other {
Knife b; // Knife in Basic
struct Knife{};
Knife c; //Knife in Other
Basic::Knife k; //Knife in Basic
}
}
namespace Jim {
template<> class SwissArmyKnife<Knife>{};
}
2.9 模板的别名
using uint = unsigned int;
//在模版中使用using,typedef无法达到这样的效果
template <typename T>
using MapString = std::map<T, char*>;
MapString<int> numberedString;
2.10 一般化的SFINAE
SFINA(substitution failture is not an error,匹配失败不是错误)。这条规则是对重载的模版的参数进行展开的时候,如果展开导致一些类型不匹配,编译器不会报错。
struct Test {
typedef int foo;
};
template <typename T>
void f(typename T::foo) {} //#1
template <typename T> //#2
void f(T) {}
int main() {
f<Test>(10); //调用#1
f<int>(10); //调用#2. 由于SFINA,虽然不存在类型int::foo,也不会发生编译错误
}
C++ 98中,一些在模版参数中使用表达式的情况,并没有被主流编译器支持。在C++ 11中,在模版参数替换时使用了表达式的情况下进行了明确规定,即表达式中没有出现“外部于表达式本身”的元素。如,发生一些模版的实例化,或者隐藏地产生一些拷贝函数,这样的模版推导都不会产生SFINAE失败(即编译器报错)。
template <int I> struct A {};
char xxx(int);
char xxx(float);
template <class T>
A<sizeof(xxx((T)0))> f(T) {} //返回值为sizeof(xxx((T)0))为参数的类模版A
int main() {
f(1);
}