cpp11特性(二)

1.继承构造函数

如果基类有多个构造函数,使用继承构造函数可以让派生类免于重写各个基类的构造函数。

struct A {
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char *c) {}
    // ...
};

struct B : A {
    using A : A;	// 继承构造函数
    // ...
}

c++11标准继承构造函数和默认构造函数、析构函数、拷贝构造一样,都是隐式声明的。不过继承构造函数只会初始化基类中的成员变量,对于派生类的成员变量无法初始化,这时候要结合初始化表达式为派生类的成员变量设定默认值。但是,继承构造函数不会继承基类的成员变量的默认值

当派生类的继承基类时,派生类的继承构造函数可能不只有一个结果:

struct A {
	A (int a = 3, double 2.4) {}
}

struct B : A {
	using A::A;
}

/*
可能生成的继承构造函数如下:
A(int=3, double=2.4)
A(int=3)
A(const A&)
A()
对应的B的构造函数将有:
B(int=3, double=2.4)
B(int=3)
B(const B&)
B()
*/

其他的规则:

  • 如果基类的构造函数被声明为私有成员函数,或者派生类是从基类中虚继承的,则不能在派生类中声明继承构造函数。
  • 如果派生类一旦使用了继承构造函数,编译器则不会为派生类构造自己的默认构造函数。
struct A {A(int) {}};
struct B : A {using A::A;};

B b;  //B没有默认构造函数,不能通过编译

2.委派构造函数

所谓委托构造就是让类中的某个构造函数去委托另一个构造函数执行构造操作的机制。一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程。调用者叫委派构造函数,被调用者叫目标构造函数。

3.右值引用:移动语义和完美转发

(1) 移动构造函数:移交所有权,析构自身。

  • 左值:可以取地址的,有名字的。

  • 右值:不能取地址,无名字的。又可细分为纯右值和将亡值。

    • 纯右值:辨识临时变量和一些不跟对象关联的值
    • 将亡值:C++11新增的跟右值引用相关的表达式,如右值引用T &&的函数返回值、std::move的返回值、或者转换为T &&的类型转换函数的返回值

为区别C++98的引用类型,之前的引用叫做左值引用,左值引用和右值引用都是引用类型,声明时都必须立即进行初始化。右值引用只能绑定右值,不能绑定任何左值。左值引用可以接受非常量左值、常量左值、右值对其进行初始化。

T && a = ReturnRvalue(); 		// √ 绑定右值

int c;
int && d = c;					// X 绑定左值

使用移动语义的前提下需要给类添加一个以右值引用为参数的移动构造函数。

而通常情况下,在为类声明了一个移动构造函数后,可以声明一个常量左值为参数的拷贝构造函数,以保证在移动构造不成时,还可以进行拷贝构造(偶尔也有特殊用途的反例)。

引用类型\可以引用的值类型 非常量左值 常量左值 非常量右值 常量右值 注记
非常量左值引用 Y N N N
常量左值引用 Y Y Y Y 全能类型、可用于拷贝语义
非常量右值引用 N N Y N 用于移动语义、完美转发
常量右值引用 N N Y Y 暂无用途

如果不知道一个类型是否属于引用类型,可以用标准库<type_traits>头文件中的3个模板类进行判断:is_rvalue_reference、is_lvalue_reference、is_reference。如判断string &&的类型:

cout << is_rvalue_reference<string &&>::value;

(2) std::move: 强制转化为右值

std::move基本等同于一个类型转换:

static_cast<T&&>(lvalue);

但注意:被std::move转化的左值,其生命期并没有随着左右值的转化而改变。

#include <iostream>
using namespace std;

class Moveable {
public:
	Moveable() : i(new int(3)) {}
    ~Moveable() { delete i; }
    Moveable(const Moveable &m) : i(new int(*m.i)) {}
    Moveable(Moveable && m) : i(m.i) {
        m.i = nullptr;
    }
    int *i;
};

int main() {
    Moveable a;
    Moveable c(move(a));   // 会调用移动构造函数
    cout << *a.i << endl;  // 运行时错误
    return 0;
}

/*
move(a)后,a.i就被移动构造函数设置为指针空值,所以后来使用会报错
*/

如上例,在大多数时候,我们需要将其转化为右值引用的还是一个确实生命期即将结束的对象。

声明了移动构造函数、移动赋值函数、拷贝赋值函数和析构函数中的一个或多个,编译器不会再为程序员生成默认的拷贝构造函数。所以拷贝构造/赋值和移动构造/赋值函数必须同时提供,或者同时不提供,这才能保证类同时具备拷贝语义和移动语义。

可以通过可以用标准库<type_traits>头文件中的is_move_constructible、is_trivially_move_constructible、is_nothrow_move_constructible判断一个类型是否可以移动:

cout << is_move_constructible<UnknownType>::value;

(3) 完美转发(perfect forwarding)

完美转发,是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

template <typename T>
void IamForwording(T t) { IrunCodeActually(t); }

对于目标函数IrunCodeActually而言,它总是希望转发函数将参数按照传入lamForwarding时的类型传递(即传入lamForwording的是左值对象,IrunCodeActually就能获得左值对象,传入lamForwording的是右值对象,lrunCodeActually就能获得右值对象),而不产生额外的开销,就好像转发者不存在一样。

C++11通过使用“引用折叠”(reference collapsing),结合新的模板推导规则实现完美转发。

引用折叠:即将复杂的未知表达式折叠为已知的简单表达式。

typedef const int T;
typedef T& TR;
TR& v = 1;		//该声明在C++98中会导致编译报错,而在c++11中会导致引用折叠

表 c++11中的引用折叠

TR的类型定义 声明v的类型 v的实际类型
T& TR A&
T& TR& A&
T& TR&& A&
T&& TR A&&
T&& TR& A&
T&& TR&& A&&

将之前的例子改写:

//改写为完美转发
template <typename T>
void IamForwording(T && t) {
    IrunCodeActually(static_cast<T &&>(t));
}

在c++11中用于完美转发的函数是forward,它和move的实际实现上差别不大,move也可以实现完美转发,但并不推荐。

template <typename T>
void IamForwording(T && t) {
    IrunCodeActually(forward(t));
}

4.显示转换操作符

explicit作用:在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。

explicit使用注意事项:

  • explicit 关键字只能用于类内部的构造函数声明上。
  • explicit 关键字作用于单个参数的构造函数。
  • explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换

5.列表初始化

不仅能用于内置类型、标准库中的容器,只要#include了<initializer_list>头文件,并且声明一个以 initialize_list< T >模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化。

#include <iostream>
#include <string>
using namespace std;

enum Gender {boy, girl};
class People {
public:
    People(initializer_list<pair<string, Gender> > l) {
        auto i = l.begin();
        for ( ; != l.end(); ++i)
            data.push_back(*i);
    }
private:
    vector<pair<string, Gender> > data;
}

函数的参数列表也可以使用初始化列表。

#include <initializer_list>
using namespace std;

void Fun(initializer_list<int> iv) {}

int main() {
    Func({1, 2});
    Func({});   //空列表
    return 0;
}

同理,类和结构体的成员函数也可以使用初始化参数列表,包括一些操作符的重载函数。

此外,初始化列表还可以用于函数返回的情况,返回一个初始化列表,通常会导致构造一个临时变量,比如:

vector<int> Func { return {1,3}; }

使用列表初始化的优势是可以防止类型收窄(narrowing)。

类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。

const int x = 1024;
const int y = 10;

char a = x;					// 收窄,但可以通过编译
char *b = new char(1024);	// 收窄,但可以通过编译

char c = {x};				// 收窄,无法通过编译
char d = {y};				// 可以通过编译,char的取值范围在-128~127
unsigned char e {-1};		// 收窄,无法通过编译

float f {7};				// 可以通过编译,这个可以转换会原类型,没有发生精度丢失,所以可以通过
int g { 2.0 f};				// 收窄,无法通过编译,2.0f是一个有精度的浮点数值,转换成整数会丢失精度
float *h = new float{1e48}; // 收窄,无法通过编译
float i = 1.21;				// 可以通过编译
posted @ 2022-12-19 15:38  无知亦乐  阅读(135)  评论(0)    收藏  举报