《深入浅出C++11之2通用为本,专用为末》

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 移动语义的一些其他问题

  1. 如果声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或者多个,编译器都不会再为程序员生成默认版本。
  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 平凡的类或结构体定义:

  1. 拥有平凡的默认构造函数和析构函数(即不定义类的构造函数,编译器自动生成一个平凡的默认构造函数)
  2. 拥有平凡的拷贝构造函数(基本等同于使用memcpy进行类型的构造)和移动构造函数
  3. 拥有平凡的拷贝赋值运算符和移动赋值运算符
  4. 不能包含虚函数以及虚基类
//使用类模版来帮助判断
template <typename T> struct std::is_trivial;

2.6.2 标准布局

  1. 所有非静态成员拥有相同的访问权限
//a和b是不同的权限,所以该匿名结构体不是标准布局
struct {
	public:
		int a;
	private:
		int b;
};

  1. 在类或者结构体继承时,满足以下两种情况之一:
  • 派生类中有非静态成员,且只有一个仅包含静态成员的基类
  • 基类有非静态成员,而派生类没有非静态成员
    非静态成员只要同时出现在派生类和基类中,即不属于标准布局
    因此一旦非静态成员出现在多个基类中,派生类也不属于标准布局
  1. 类中第一个非静态成员的类型与其基类不同
    C++标准中,如果基类没有成员,标准允许派生类的第一个成员与基类共享地址。但是如果基类的第一个成员仍然是基类类型,编译器仍会为基类分配1字节的空间,因为C++标准要求类型相同的对象必须地址不同(基类地址与派生类中的成员b的地址必须不同)
//A不是一个标准布局的类型,因为第一个非静态成员变量b的类型跟A所继承的类型B相同。
struct A : B { B b;}

  1. 没有虚函数和虚基类
  2. 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局。
//使用类模版来帮助判断
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);
}

猜你喜欢

转载自blog.csdn.net/kaydxh/article/details/106302956