第34章 基础特性的其他优化(C++11~C++20)
34.1 显式自定义类型转换运算符(C++11)
#include <iostream>
#include <vector>
template<class T>
class SomeStorage {
public:
SomeStorage() = default;
SomeStorage(std::initializer_list<T> l) : data_(l) {};
operator bool() const { return !data_.empty(); }
private:
std::vector<T> data_;
};
int main()
{
SomeStorage<float> s1{ 1., 2., 3. };
SomeStorage<int> s2{ 1, 2, 3 };
std::cout << std::boolalpha;
std::cout << "s1 == s2 : " << (s1 == s2) << std::endl;
std::cout << "s1 + s2 : " << (s1 + s2) << std::endl;
}
输出结果:
s1 == s2 : true
s1 + s2 : 2
在s1和s2比较和相加的过程中,编译器会对它们做隐式的自定义类型转换以符合比较和相加的条件。由于这两个对象都不为空,因此它们的返回值都为true,s1 == s2的运算结果自然也为true,而求和运算会将bool转换为int,于是输出运算结果为2。
#include <iostream>
#include <string.h>
class SomeString {
public:
SomeString(const char * p) : str_(strdup(p)) {}
SomeString(int alloc_size) : str_((char *)malloc(alloc_size)) {}
~SomeString() { free(str_); }
private:
char *str_;
friend void PrintStr(const SomeString& str);
};
void PrintStr(const SomeString& str)
{
std::cout << str.str_ << std::endl;
}
int main()
{
PrintStr("hello world");
PrintStr(58); // 代码写错,却编译成功
}
C++已经考虑到了构造函数面临的这种问题,我们可以使用explicit说明符将构造函数声明为显式,这样隐式的构造无法通过编译:
class SomeString {
public:
SomeString(const char * p) : str_(_strdup(p)) {}
explicit SomeString(int alloc_size) : str_((char*)malloc(alloc_size)) {}
~SomeString() { free(str_); }
private:
char *str_;
friend void PrintStr(const SomeString& str);
};
int main()
{
PrintStr("hello world");
PrintStr(58); // 编译失败
PrintStr(SomeString(58));
}
C++11标准将explicit引入自定义类型转换中,称为显式自定义类型转换。
#include <iostream>
#include <vector>
template<class T>
class SomeStorage {
public:
SomeStorage() = default;
SomeStorage(std::initializer_list<T> l) : data_(l) {};
explicit operator bool() const { return !data_.empty(); }
private:
std::vector<T> data_;
};
int main()
{
SomeStorage<float> s1{ 1., 2., 3. };
SomeStorage<int> s2{ 1, 2, 3 };
std::cout << std::boolalpha;
std::cout << "s1 == s2 : " << (s1 == s2) << std::endl; // 编译失败
std::cout << "s1 + s2 : " << (s1 + s2) << std::endl; // 编译失败
std::cout << "s1 : " << static_cast<bool>(s1) << std::endl;
std::cout << "s2 : " << static_cast<bool>(s2) << std::endl;
if (s1) {
std::cout << "s1 is not empty" << std::endl;
}
}
34.2 关于std::launder()(C++17)
struct X { const int n; };
union U { X x; float f; };
接下来聚合初始化联合类型U:
U u = {{ 1 }};
使用replace new的方法重写初始化这块内存区域:
X *p = new (&u.x) X {2};
由于u.x.n是一个常量且初始化为1,因此编译器有理由认为u.x.n是无法被修改的,通过一些优化后u.x.n的结果有可能为1。实际上在标准看来,这个结果是未定义的。在经过replace new的操作后,我们不能直接使用u.x.n,只能通过p来访问n。
C++标准规定:如果新的对象在已被某个对象占用的内存上进行构建,那么原始对象的指针、引用以及对象名都会自动转向新的对象,除非对象是一个常量类型或对象中有常量数据成员或者引用类型。简单来说就是,如果数据结构X的数据成员n不是一个常量类型,那么u.x.n的结果一定是2。但是由于常量性的存在,从语法规则来说x已经不具备将原始对象的指针、引用以及对象名自动转向新对象的条件,因此结果是未定义的,要访问n就必须通过新对象的指针p。
引入std::launder()就是为了解决上述问题
assert(*std::launder(&u.x.n) == 2);
launder在英文中有清洗和刷洗的意思。而在这里不妨理解为洗内存,它的目的是防止编译器追踪到数据的来源以阻止编译器对数据的优化。
34.3 返回值优化(C++11~C++17)
严格来说返回值优化分为RVO(Return Value Optimization)和NRVO(Named Return Value Optimization),不过在优化方法上的区别并不大,一般来说当返回语句的操作数为临时对象时,我们称之为RVO;而当返回语句的操作数为具名对象时,我们称之为NRVO。
#include <iostream>
#include <ctime>
class X {
public:
X() { std::cout << "X ctor" << std::endl; }
X(const X&x) { std::cout << "X copy ctor" << std::endl; }
~X() { std::cout << "X dtor" << std::endl; }
};
X make_x()
{
X x1, x2;
if (std::time(nullptr) % 50 == 0) {
return x1;
}
else {
return x2;
}
}
int main()
{
X x3 = make_x();
}
输出结果:
X ctor
X ctor
X copy ctor
X dtor
X dtor
X dtor
由于以上代码中究竟由x1还是x2复制到x3是无法在编译期决定的,因此编译器无法在默认构造阶段就对x3进行构造,它需要分别将x1和x2构造后,根据运行时的结果将x1或者x2复制构造到x3,在这个过程中返回值优化技术也尽其所能地将中间的临时对象优化掉了,所以这里只会看到一次复制构造函数的调用。
34.4 允许按值进行默认比较(C++20)
以下代码在C++20标准之前是无法编译成功的
struct C {
int i;
friend bool operator==(C, C) = default;
};
在C++20之前的标准中,类的默认比较规则要求类C可以有一个参数为const C&的非静态成员函数,或者有两个参数为const C&的友元函数。而C++20标准对这一条规则做了适度的放宽,它规定类的默认比较运算符函数可以是一个参数为const C&的非静态成员函数,或是两个参数为const C&或C的友元函数。
下面这两种情况依旧是标准不允许的:
struct A {
friend bool operator==(A, const A&) = default;
};
struct B {
bool operator==(B) const = default;
};
A因为混用const A&和A而不符合标准要求,所以编译失败。另外,标准并没有放宽默认比较中对于非静态成员函数的要求,B依然无法通过编译。
34.5 支持new表达式推导数组长度(C++20)
在用new表达式声明数组的时候无法把推导数组长度的任务交给编译器
int *x = new int[]{ 1, 2, 3 };
char *s = new char[]{ "hello world" };
C++20标准解决了以上问题。提案文档中强调在数组声明时根据初始化元素个数推导数组长度的特性应该是一致的,所以用以上方式声明数组理应是一个合法的语法规则。
34.6 允许数组转换为未知范围的数组(C++20)
在C++20标准中允许数组转换为未知范围的数组,例如:
void f(int(&)[]) {}
int arr[1];
int main()
{
f(arr);
int(&r)[] = arr;
}
34.7 在delete运算符函数中析构对象(C++20)
通常情况下delete一个对象,编译器会先调用该对象的析构函数,之后才会调用delete运算符删除内存
#include <new>
struct X {
X() {}
~X()
{
std::cout << "call dtor" << std::endl;
}
void* operator new(size_t s)
{
return ::operator new(s);
}
void operator delete(void* ptr)
{
std::cout << "call delete" << std::endl;
::operator delete(ptr);
}
};
X* x = new X;
delete x;
输出结果:
call dtor
call delete
从C++20标准开始,这个过程可以由我们控制了
struct X {
X() {}
~X()
{
std::cout << "call dtor" << std::endl;
}
void* operator new(size_t s)
{
return ::operator new(s);
}
void operator delete(X* ptr, std::destroying_delete_t)
{
std::cout << "call delete" << std::endl;
::operator delete(ptr);
}
};
在这种情况下,我们需要自己调用析构函数:
void operator delete(X* ptr, std::destroying_delete_t)
{
ptr->~X();
std::cout << "call delete" << std::endl;
::operator delete(ptr);
}
34.8 调用伪析构函数结束对象声明周期(C++20)
C++20标准完善了调用伪析构函数结束对象声明周期的规则。
template<typename T>
void destroy(T* p) {
p->~T();
}
当T是非平凡类型时,p->T();会结束对象声明周期;相反当T为平凡类型时,比如int类型,p->T();会被当成无效语句。C++20标准修补了这种行为不一致的规则,它规定伪析构函数的调用总是会结束对象的生命周期,即使对象是一个平凡类型。
34.9 修复const和默认复制构造函数不匹配造成无法编译的问题(C++20)
考虑这样一个类或者结构体,它编写复制构造函数的时候没有使用const:
struct MyType {
MyType() = default;
MyType(MyType&) {};
};
template <typename T>
struct Wrapper {
Wrapper() = default;
Wrapper(const Wrapper&) = default;
T t;
};
Wrapper<MyType> var;
Wrapper的复制构造函数的形参是const版本而其成员MyType不是,这种不匹配在C++17和以前的标准中是不被允许的。但仔细想想,这样的规定并不合理,因为代码并没有试图去调用复制构造函数。在C++20标准中修正了这一点,如果不发生复制动作,这样的写法是可以通过编译的。
34.10 不推荐使用volatile的情况(C++20)
C++20标准在部分情况中不推荐volatile的使用,这些情况包括以下几种。
1.不推荐算术类型的后缀++和–表达式以及前缀++和–表达式使用volatile限定符
2.不推荐非类类型左操作数的赋值使用volatile限定符
3.不推荐函数形参和返回类型使用volatile限定符
4.不推荐结构化绑定使用volatile限定符
34.11 不推荐在下标表达式中使用逗号运算符(C++20)
对于逗号运算符我们再熟悉不过了,它可以让多个表达式按照从左往右的顺序进行计算,整体的结果为系列中最后一个表达式的值
int a[]{ 1,2,3 };
int x = 1, y = 2;
std::cout << a[x, y];
从C++20标准开始,std::cout << a[x, y];这句代码会被编译器提出警告,因为标准已经不推荐在下标表达式中使用逗号运算符了。
34.12 模块(C++20)
模块(module)是C++20标准引入的一个新特性,它的主要用途是将大型工程中的代码拆分成独立的逻辑单元,以方便大型工程的代码管理。模块能够大大减少使用头文件带来的问题,例如在使用头文件时经常会遇到宏和函数的重定义,而模块则会好很多,因为宏和未导出名称对于导入模块是不可见的。使用模块也能大幅提升编译效率,因为编译后的模块信息会存储在一个二进制文件中,编译器对于它的处理速度要远快于单纯使用文本替换的头文件方法。
// helloworld.ixx
export module helloworld;
import std.core;
export void hello() {
std::cout << "Hello world!\n";
}
// modules_test.cpp
import helloworld;
int main()
{
hello();
}
helloworld.ixx是接口文件,它将编译成一个名为helloworld的导出模块。在模块中使用import引入了std.core,std.core是一个STL模块,包含了STL中最主要的容器和算法。除此之外,模块还使用export导出了一个hello函数。编译器编译helloworld.ixx会生成一个helloworld.ifc,该文件包含了模块的元数据。modules_test.cpp可以通过importhelloworld;导入helloworld模块,并且调用它的导出函数hello。
在使用VS 2019进行编译时有两点需要注意。
1.在安装VS 2019的C++环境时勾选模块(默认不勾选)。如果不
做这一步,会导致import std.core;无法正确编译。
2.编译选项开启/experimental:module