深入理解C++11【2】

时间:2023-03-09 15:07:29
深入理解C++11【2】

深入理解C++11【2】

1、继承构造函数。

  当基类拥有多个构造函数的时候,子类不得不一一实现。

  C++98 可以使用 using 来使用基类的成员函数。

#include < iostream> using namespace std;
struct Base {
void f( double i){
cout << "Base:" << i << endl;
}
};
struct Derived : Base {
using Base:: f;
void f( int i) {
cout << "Derived:" << i << endl;
}
}; int main() {
Base b;
b. f( . ); // Base: 4. 5 Derived d;
d. f( . ); // Base: 4. 5
} // 编译 选项: g++ 3- 1- 3. cpp

  C++11中,这个功能由成员函数扩展到了构造函数上。

struct A {
A( int i) {}
A( double d, int i) {}
A( float f, int i, const char* c) {} // ...
}; struct B : A {
using A:: A; // 继承 构造 函数
// ...
virtual void ExtraInterface(){}
};

  这 意味着 如果 一个 继承 构造 函数 不被 相关 代码 使用, 编译器 不 会为 其 产生 真正 的 函数 代码。

  对于 继承 构造 函数 来讲, 参数 的 默认值不会被继承 的。 事实上, 默认值 会 导致 基 类 产生 多个 构造 函数 的 版本, 这些 函数 版本 都会 被 派生 类 继承。

  如果 一旦 使用 了 继承 构造 函数, 编译器 就不 会 再为 派生 类 生成 默认 构造 函数 了,

struct A { A (int){} };
struct B : A{ using A:: A; };
B b; // B 没有 默认 构造 函数

  截至2013年,还没有编译器实现了继承构造函数。

2、委派构造函数。

  C++98 中编译器 不允许 在 构造 函数 中 调用 构造 函数, 即使 参数 看起来 并不 相同。以下代码会产生编译错误。

Info() { InitRest(); } 
Info( int i) { this-> Info(); type = i; }
Info( char e) { this-> Info(); name = e; }

  所以C++98的hacker喜欢使用 placement new 来实现调用其它构造函数。

Info() { InitRest(); }
Info( int i) { new (this) Info(); type = i; }
Info( char e) { new (this) Info(); name = e; }

  C++11 中可以使用委派构造函数来简化代码。

class Info {
public:
Info() { InitRest(); }
Info( int i) : Info() { type = i; }
Info( char e): Info() { name = e; }
private:
void InitRest() { /* 其他 初始化 */ }
int type {};
char name {'a'}; // ...
}; // 编译 选项: g++ -c -std= c++ 11 3- 2- 3. cpp

  调用“ 基准 版本” 的 构造 函数 为 委派 构造 函数( delegating constructor), 而被 调用 的“ 基准 版本” 则为 目标 构造 函数( target constructor)。

  构造 函数 不能 同时“ 委派” 和 使用 初始化 列表, 所以 如果 委派 构造 函数 要给 变量 赋 初值, 初始化 代码 必须 放在 函数 体中。如下:

struct Rule1 {
int i;
Rule1( int a): i( a) {}
Rule1(): Rule1( ), i( ) {} // 无法 通过 编译
};

  目标 构造 函数 的 执行 总是 先于 委派 构造 函数 而 造成 的。

  委派 构造 的 一个 很 实际 的 应用 就是 使用 构造模板函数 产生 目标 构造 函数,

class TDConstructed
{
template< class T>
TDConstructed( T first, T last) : l( first, last) {}
list< int> l;
public:
TDConstructed( vector< short> & v): TDConstructed( v. begin(), v. end()) {}
TDConstructed( deque< int> & d): TDConstructed( d. begin(), d. end()) {}
}; // 编译 选项: g++ -c -std= c++ 11 3- 2- 6. cpp

  委托构造函数的异常捕获稍稍有些另类,try-catch 是在函数外:

class DCExcept {
public:
DCExcept( double d) try : DCExcept( , d) {
cout << "Run the body." << endl; // 其他 初始化
}
catch(...) {
cout << "caught exception." << endl;
}

3、移动语义

  C++98 中的经典问题,会产生多达分别3次的construct、destruct:

HasPtrMem GetTemp() {
return HasPtrMem();
}
int main() {
HasPtrMem a = GetTemp();
}
// 编译 选项: g++ 3- 3- 3. cpp -fno- elide- constructors

  输出如下:

Construct:
Copy construct:
Destruct:
Copy construct:
Destruct:
Destruct:

  深入理解C++11【2】

  移动构造函数与 Copy构造函数:

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;
}
Construct:  Resource from GetTemp: 0x603010
Move construct:
Destruct:
Move construct:
Destruct:
Resource from main: 0x603010
Destruct:

4、左值、右值、右值引用

  1)等号左边的是“左值”。等号右边的是“右值”。

  2)可以取地址的、有名字是左值。不能取地址、没有名字的就是右值。

  C++11程序中,所有值必属于左值、将亡值、右值。

  右值引用是不能绑定到任何左值的,如下代码就无法通过编译:

int c;
int && d = c;

  常量左值引用可以接受右值,同时也像右值引用一样将右值生命周期延长。如下第一行:

const bool & judgement = true;
const bool judgement = true;

  C++98中也常可以使用常量左值引用来减少临时对象的开销。

#include < iostream>
using namespace std;
struct Copyable {
Copyable() {}
Copyable( const Copyable &o) {
cout << "Copied" << endl;
}
}; Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal( Copyable) {}
void AcceptRef( const Copyable & ) {} int main()
{
cout << "Pass by value: " << endl;
AcceptVal( ReturnRvalue()); // 临 时值 被 拷贝 传入 cout << "Pass by reference: " << endl;
AcceptRef( ReturnRvalue()); // 临 时值 被 作为 引用 传递
} // 编译 选项: g++ 3- 3- 5. cpp -fno- elide- constructors
Pass by value:
Copied
Copied
Pass by reference:
Copied

  std::move 的作用是强制一个左值成为右值。

  因为 const T& 是万能引用,可以指向右值。所以当类实例无 MoveConstructor时,将会调用 CopyConstructor来替代。

  但 const T&& 常量右值引用没有卵用,一来MoveConstructor需要引用右值;二来如果右值不能改,使用 const T& 就可以了。所以 const T&&没有卵用。

  深入理解C++11【2】

  为了知道一个类型是否是引用类型,可以使用 <type_traits>头文件中的3个模板类:

  1)is_rvalue_reference

  2)is_lvalue_reference

  3)is_reference

5、std::move

  std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用。std::move 基本赞同于一个类型转换:

static_ cast< T&&>( lvalue);

  使用 std::move 时,我们希望转换为右值引用的这个值是一个生命期即将结束的对象。一个正确的 std::move 的使用场景。

class HugeMem{
public:
HugeMem( int size): sz( size > ? size : ) { c = new int[ sz]; }
~ HugeMem() { delete [] c; } HugeMem( HugeMem && hm): sz( hm. sz), c( hm. c) { hm. c = nullptr; }
int * c;
int sz;
}; class Moveable{
public:
Moveable(): i( new int( )), h( ) {}
~ Moveable() { delete i; } Moveable( Moveable && m): i( m. i), h( move( m. h)) { // 强制 转为 右 值, 以 调用 移动 构造 函数
m. i = nullptr;
}
int* i;
HugeMem h;
};

6、移动语义的其它问题。

  下述代码会使得 的 临时 变量 常 量化, 成为 一个 常量 右 值, 那么 临时 变量 的 引用 也就 无法 修改, 从而 导致 无法 实现 移动 语义。

Moveable( const Moveable &&)
const Moveable ReturnVal();

  编译器 会为 程序员 隐式 地 生成 一个( 隐式 表示 如果 不被 使用 则 不 生成) 移动 构造 函数。 不过 如果 程序员 声明 了 自定义 的 拷贝 构造 函数、 拷贝 赋值 函数、 移动 赋值 函数、 析 构 函数 中的 一个 或者 多个, 编译器 都不 会 再为 程序员 生成 默认 版本。

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

  只有 移动 语义 的 类型 则 非常 有趣, 因为 只有 移动 语义 表明 该 类型 的 变量 所 拥有 的 资源 只能 被 移动, 而 不能 被 拷贝。 那么 这样 的 资源 必须 是 唯一 的。 因此, 只有 移动 语义 构造 的 类型 往往 都是“ 资源 型” 的 类型, 比如说 智能 指针, 文件 流 等,

  有了移动语义,可以实现高性能的 swap 函数。

template < class T>
void swap( T& a, T& b)
{
T tmp( move( a));
a = move( b);
b = move( tmp);
}

  程序员 应该 尽量 编写 不 抛出 异常 的 移动 构造 函数, 通过 为 其 添加 一个 noexcept 关键字, 可以 保证 移动 构造 函数 中 抛出 来的 异常 会 直接 调用 terminate 程序 终止 运行, 而 不是 造成 指针 悬挂 的 状态。

  一个 std:: move_ if_ noexcept 的 模板 函数 替代 move 函数。 该 函数 在 类 的 移动 构造 函数 没有 noexcept 关键字 修饰 时 返回 一个 左 值 引用 从而 使 变量 可以 使用 拷贝 语义, 而在 类 的 移动 构造 函数 有 noexcept 关键字 时, 返回 一个 右 值 引用, 从而 使 变量 可以 使用 移动 语义。如下:

  

struct Maythrow
{
Maythrow() {} Maythrow( const Maythrow&) {
std:: cout << "Maythorow copy constructor." << endl;
} Maythrow( Maythrow&&) {
std:: cout << "Maythorow move constructor." << endl;
}
}; struct Nothrow
{
Nothrow() {}
Nothrow( Nothrow&&) noexcept {
std:: cout << "Nothorow move constructor." << endl;
} Nothrow( const Nothrow&) {
std:: cout << "Nothorow move constructor." << endl;
}
}; int main()
{
Maythrow m;
Nothrow n; Maythrow mt = move_ if_ noexcept( m); // Maythorow copy constructor.
Nothrow nt = move_ if_ noexcept( n); // Nothorow move constructor.
return ;
} // 编译 选项: g++ -std= c++ 11 3- 3- 8. cpp

  在 本节 中 大量 的 代码 都 使用 了- fno- elide- constructors 选项 在 g++/ clang++ 中 关闭 这个 优化。如果关闭,则下述代码只会有一次 constructor、destructor:

A ReturnRvalue() { A a(); return a; }
A b = ReturnRvalue();

7、完美转发

  完美 转发( perfect forwarding), 是指 在 函数 模板 中, 完全 依照 模板 的 参数 的 类型, 将 参数 传递 给 函数 模板 中 调用 的 另外 一个 函数。下面的例子中,如果传入的是一个右值,t被传给内部函数时成了一个左值,不是完美转发。

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

  我们希望传入左值,传出也是左值;传入右值,传出也是右值。

  C++11加入了引用折叠新规则。在C++98中,以下代码会导致编译错误。

typedef const int T;
typedef T& TR;
TR& v = ; // 该 声明 在 C++ 98 中会 导致 编译 错误

  深入理解C++11【2】

  

  模板类型 的 推导 规则 就比 较 简单, 当 转发 函数 的 实 参 是 类型 X 的 一个 左 值 引用, 那么 模板 参数 被 推导 为 X& 类型, 而 转发 函数 的 实 参 是 类型 X 的 一个 右 值 引 用的 话, 那么 模板 的 参数 被 推导 为 X&& 类型。

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

  上面就是完美转发。

  

8、显式类型转换

  只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象。

class String
{
public:
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
} String s1 = “hello”; //OK 隐式转换,等价于String s1 = String(”hello”)

  在 C++ 11 中, 标准 将 explicit 的 使用范围 扩展到 了 自定义 的 类型 转换 操作 符 上。

class ConvertTo {}; 

class Convertable
{
public:
explicit 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); // 拷贝 构造 初始化, 编译 失败
}
// 编译 选项: g++ -std= c++ 11 3- 4- 3. cpp

9、初始化列表

  C++98 中只允许对数组进行列表初始化,C++11扩展到了所有元素。

#include < vector>
#include < map>
using namespace std;
int a[] = {, , }; // C++ 98 通过, C++ 11 通过
int b[] {, , }; // C++ 98 失败, C++ 11 通过
vector< int> c{ , , }; // C++ 98 失败, C++ 11 通过
map< int, float> d = {{, . 0f}, {, . 0f} , {, . 2f}}; // C++ 98 失败, C++ 11 通过
// 编译 选项: g++ -c -std= c++ 11 3- 5- 1. cpp

  如上,列表 初始化 可以 在“{}” 花 括号 之前 使用 等号, 其 效果 与 不带 使用 等号 的 初始化 相同。

  {}和()一样,也可以用于 new操作符中。

int * i = new int( );
double * d = new double{ . 2f};

  自定义类型如需要初始化列表功能,需要提供一个构造函数,此函数使用唯一一个参数,initialize_list。

enum Gender {boy, girl}; 

class People {
public:
People( initializer_ list< pair< string, Gender>> l)
{
// initializer_ list 的 构造 函数
auto i = l. begin();
for (;i != l. end(); ++ i)
data. push_ back(* i);
} private:
vector< pair< string, Gender>> data;
}; People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}}; // 编译 选项: g++ -c -std= c++ 11 3- 5- 2. cpp

  函数也可以使用初始化列表:

void Fun( initializer_ list< int> iv){ } 

int main() {
Fun({ , });
Fun({}); // 空 列表
} // 编译 选项: g++ -std= c++ 11 3- 5- 3. cpp

  当初始化列表 + operator[] + operator= 时,会产生强大的语法效果:

using namespace std; 

class Mydata
{
public:
Mydata & operator [] (initializer_ list< int> l)
{
for (auto i = l. begin(); i != l. end(); ++ i)
idx. push_ back(* i);
return *this;
} Mydata & operator = (int v)
{
if (idx. empty() != true)
{
for (auto i = idx. begin(); i != idx. end(); ++ i)
{
d. resize((* i > d. size()) ? *i : d. size());
d[* i - ] = v;
} idx. clear();
}
return *this;
} void Print()
{
for (auto i = d. begin(); i != d. end(); ++ i)
cout << *i << " "; cout << endl;
} private:
vector< int> idx; // 辅助 数组, 用于 记录 index
vector< int> d;
}; int main()
{
Mydata d;
d[{ , , }] = ;
d[{ , , , }] = ;
d. Print(); // 4 7 7 4 4 0 0 4
}
// 编译 选项: g++ -std= c++ 11 3- 5- 4. cpp

  

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

vector< int> Func() { return {, }; }

10、类型收窄

  在 C++ 11 中, 使用 初始化 列表 进行 初始化 的 数据 编译器 是 会 检查 其是 否 发生 类型 收 窄 的。

const int x = ;
const int y = ;
char a = x; // 收 窄, 但可以 通过 编译
char* b = new char( ); // 收 窄, 但可以 通过 编译
char c = {x}; // 收 窄, 无法 通过 编译
char d = {y}; // 可以 通过 编译
unsigned char e {-}; // 收 窄, 无法 通过 编译
float f { }; // 可以 通过 编译
int g { . 0f }; // 收 窄, 无法 通过 编译
float * h = new float{ 1e48}; // 收 窄, 无法 通过 编译
float i = . 2l; // 可以 通过 编译
// 编译 选项: clang++ -std= c++ 11 3- 5- 5. cpp

  在 C++ 11 中, 列表 初始化 是 唯一 一种 可以 防止 类型 收 窄 的 初始化 方式。 这也 是 列表 初始化 区别于 其他 初始化 方式 的 地方。

11、

12、

13、