
语言可用性增强

关于C++(Modern)中对语言可用性的强化
语言可用性的强化
常量
nullptr VS NULL
nullptr出现的目标是为了替代NULL,传统C++会把NULL,0视为同一种东西,这取决于编译器如何定义NULL,有些编译器会把NULL定义为((void*)0)
,有些则会直接将其定义为0
而在C++中不允许直接将void *
隐式转换到其他类型
:kissing: example
1 | void foo(char *); |
C++11引入nullptr
关键字,专门用来区分空指针和0;nullptr
的类型为nullptr_t
,能够隐式的转换为任何指针或成员指针的类型
constexpr
const
常数不等同于常量表达式
:worried: example
1 | const int len = 1 + 1; |
上面这种行为在大多数编译器中都支持,但是这是一个非法的行为
tips: 现在大部分编译器都带有自身编译优化,很多非法行为在编译器的优化的加持下会变得合法
C++11提供了constexpr
让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证常量表达式
constexpr
修饰的函数可以使用递归:
:grinning: example
1 | constexpr int fibonacci(const int n) { |
C++14开始,constexpr
函数可以在内部使用局部变量,循环和分支等简单语句
:grinning: example
1 | // C++14起可以通过编译 |
变量及其初始化
if/switch变量声明强化
C++17起,可在if和switch语句中声明一个临时变量
:astonished: example
1 | if (const auto iter = std::find(vec.begin(), vec.end(), 3); iter != vec.end()) { |
初始化列表
在传统C++中,不同对象有着不同的初始化方法;对于数组和POD类型都可以使用{}进行初始化,而对于类对象的初始化,要么需要拷贝构造、要么就需要使用()进行
为了解决这个问题,C++11把初始化列表的概念绑定到类型上std::initializer_list
,允许构造函数像其他函数参数一样使用初始化列表,为类对象的初始化与普通数组和POD的初始化方法提供统一的桥梁
:weary: example
1 | class Foo { |
结构化绑定
结构化绑定提供了类似其他语言中提供的多返回值的功能
C++11/14给我们提供了std::tuple
来构造一个元组,进而囊括多个返回值;但并未提供简单的方式从元组中拿到并定义元组中的元素(使用std::tie
对元组拆包);C++17给出了结构化绑定
:yum: example
1 | std::tuple<int, double, std::string> f() { |
类型推导
C++11引入了auto
和decltype
实现类型推导
auto
tips: auto很早就存在于C++,但始终作为一个存储类型的指示符,与register并存
C++20起,auto甚至能用于函数传参
:confused: example
1 | int add(auto x, auto y) { |
decltype
decltype
关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的,类似于typeof(非C++标准)
:sunglasses: example
1 | decltype(expr); |
尾返回类型推导
在传统C++中我们必须这么做:
1 | template<typename R, typename T, typename U> |
C++11起这个问题得以解决:
1 | decltype(x + y) add(T x, U y); |
但是这种写法并不能通过编译,C++11引入尾返回类型(trailing return type),利用auto关键字将返回类型后置
:hushed: example
1 | template<typename T, typename U> |
C++14起可以直接让普通函数具备返回值推导,下面这种写法也就合法了
1 | template<typename T, typename U> |
decltype(auto)
涉及参数转发,decltype(auto)
主要用于对转发函数或封装的返回值类型进行推导,使得我们无需显式指定decltype的参数表达式
example
1 | std::string lookup1(); |
控制流
if constexpr
C++11引入constexpr
关键字,将表达式或函数编译为常量结果,将其加入到条件判断中让代码在编译期就完成分支判断
:smirk: example
1 | template<typename T> |
区间for迭代
C++11引入基于范围的迭代写法
1 | // in c++ 17 |
1 | // in c++ 20 |
模板
模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能
外部模板
在传统C++中,模板只有在使用时才会被编译期实例化:只要在每个编译单元中编译的代码中遇到被完整定义的模板,都会实例化(产生了重复实例化而导致的编译时间增加,并且没有办法通知编译器不要触发模板的实例化)
C++11引入外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得我们能够显式的通知编译器何时进行模板的实例化
1 | template class std::vector<bool>; // 强制实例化 |
类型别名模板
模板是用来产生类型的,在传统C++中,typedef
可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称
C++11使用using
引入了下面这种形式的写法,并且同时支持对传统typedef
相同的功效
1 | using NewName = OldName<xxx, xxx>; |
变长参数模板
C++11加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定
1 | template<typename... Ts> class Magic; |
模板类Magic的对象,能够接受不受限制个数的typename作为模板的形式参数(包括0个参数)
使用变长参数模板实现变长参数函数
1 | template<typename... Args> void printf(const std::string &str, Args... args); |
对参数解包
:smirk: example
1 | template<typename... Ts> |
递归模板函数
1
2
3
4
5
6
7
8
9template<typename T0>
void printf(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}变参模板展开
C++17中增加了变参模板展开的支持
1
2
3
4
5
6template<typename T0, typename... T>
void printf(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0)
printf(t...);
}初始化列表展开
黑魔法(利用了
std::initializer_list
强制顺序求值的特性)1
2
3
4
5
6
7template<typename T, typename... Ts>
auto printf(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
折叠表达式
1 | template<typename... T> |
非类型模板参数推导
使用不同字面量作为模板参数
:satisfied: example
1 | template<typename T, int BufSize> |
tips: 也可使用auto来进行类型推导
1 | template<auto value> void foo() { |
面向对象
final keyword
C++11引入:限制类的继承以及阻止成员函数的覆盖
阻止类被继承
1
2class Base final { };
class Derived : public Base { /* 编译错误:Base 是一个 final 类,不能被继承 */ };阻止派生类中覆盖方法
1
2
3
4
5
6
7
8class Base {
public:
virtual void foo() final { /* foo 函数不能在派生类中被覆盖 */ }
};
class Derived : public Base {
public:
void foo() override { /* 编译错误:foo 被声明为 final,不能被覆盖 */ }
};
static members
类的静态成员是与类本身相关联的成员,而不是与类的各个实例相关联的
静态成员变量
- 共享:静态成员变量为类的所有对象所共享。无论创建了多少个类的实例,都只有一个静态成员变量的拷贝
- 独立于任何对象:即使没有创建类的实例,静态成员变量也存在,它们与类类型自身关联,而不是与特定的实例关联
- 初始化:静态成员变量需要在类定义外进行初始化(通常在源文件中),对于整型或枚举类型的常量静态成员,可以在类内直接初始化
- 访问:静态成员变量可以通过类名和作用域解析运算符
::
访问,也可以通过类的对象或引用访问
静态成员函数
- 独立于对象:静态成员函数不依赖于类的任何特定实例。它们不能访问类的非静态成员变量,也不能调用非静态成员函数
- 全局访问:静态成员函数可以在不创建类的实例的情况下调用
- 访问限制:静态成员函数遵循正常的访问控制规则(
public
、protected
、private
)
:confused: example
1 | class MyClass { |
静态成员变量通常用于存储与类相关的信息,如对象计数器或类特定的常量值
静态成员函数常用于实现与类的具体实例无关的功能,如工厂方法或辅助函数
构造函数
Rule of 3/5
:astonished: Good practice: The rule of 3/5(C++98/C++11)
Rule of three
如果你的类需要自定义析构函数、拷贝构造函数或拷贝赋值操作符中的任何一个,那么你很可能需要自定义所有这三个
这是因为自定义析构函数通常意味着你的类在管理(如分配和释放)资源,这通常需要自定义的拷贝控制来正确地复制或赋值这些资源
Rule of five
规则五包括规则三中的三个函数(析构函数、拷贝构造函数、拷贝赋值操作符)以及两个新的函数:移动构造函数和移动赋值操作符
如果你需要自定义析构函数,你很可能也需要自定义所有五个函数,以正确处理移动语义,从而提高效率
构造函数中的初始化列表
:+1: 成员初始化列表提供的值优先于类定义处的默认值
成员初始化列表直接初始化成员变量,而不是先默认初始化然后赋值;对于某些类型(如引用和常量成员),这是必需的,因为它们不能被赋值
其次就是在效率方面:对于非内置类型的成员,使用初始化列表通常比在构造函数中赋值更高效,避免了额外的构造和赋值
默认构造函数
定义:如果一个构造函数可以不带任何参数被调用(或者所有参数都有默认值),则称为默认构造函数
作用:在不提供任何初始化值的情况下创建对象
示例:
1
2
3
4
5class Example {
public:
Example() = default; // 保持了特殊成员函数的自动生成:比如拷贝构造函数,析构函数等
Example() { } // 破坏了类的聚合类型属性,可能会阻止编译器生成其他特殊成员函数
};
行为:
- 对于内置类型的成员:默认构造函数不会初始化这些成员,它们的初始值是未定义的,除非它们被显示初始化
- 对于类类型的成员:这些成员的默认构造函数将被调用来初始化它们
- 对于继承的情况:如果类是从其他类继承的,基类的默认构造函数将被调用
- 对于带有默认成员初始化的成员:在C++11及更高版本中,可以在类定义中直接给成员变量赋初值,如果这样做了,即使是默认的构造函数,这些成员也会被初始化为指定的值
参数化构造函数
定义:带有一个或多个参数的构造函数,用于根据提供的参数初始化对象
作用:提供更灵活的初始化方式,允许在创建对象时指定初始值
示例:
1
2
3
4class Example {
public:
Example(...args) { /* 构造函数体 */ }
};
行为:
- 实现类型转换:如果参数化构造函数没有被声明为
explicit
,它还可能用于隐式类型转换
拷贝构造函数
定义:参数为对同类型对象的引用(通常为常量引用)的构造函数
作用:用于通过复制另一个同类型对象来初始化新对象
示例:
1
2
3
4class Example {
public:
Example(const Example & other) { /* 拷贝构造函数体 */ }
};
行为:
- 创建对象副本:拷贝构造函数通过接收一个同类型对象的引用作为参数来创建新对象的副本。这个过程包括复制现有对象所有的成员变量的值
- 被隐式调用:当函数通过值传递或者函数返回对象时,拷贝构造函数会被隐式调用
移动构造函数(C++11引入)
定义:参数为同类型对象的右值引用的构造函数
作用:允许资源的转移,提高效率,尤其用于临时对象
示例:
1
2
3
4class Example {
public:
Example(Example && other) { /* 移动构造函数体 */ }
};
行为:
- 参数类型:移动构造函数接受一个同类型对象的右值引用作为参考
- 资源转移:与拷贝构造函数(进行深拷贝)不同,移动构造函数设计用来窃取源对象的资源。这意味着它通过将资源从源对象转移到新创建的对象,来实现快速构造,而不是复制资源
- 优化临时对象:移动构造函数特别适用于临时对象或将要销毁的对象,因为这些情况下资源转移是安全的。比如在函数返回临时对象或进行对象赋值时,移动构造函数可以显著提高效率
- 自动生成规则:如果一个类没有自定义拷贝构造函数、拷贝赋值操作符、移动赋值操作符以及析构函数,编译器可能会自动生成移动构造函数
委托构造函数(C++11引入)
定义:一个构造函数在其初始化列表中调用同一类的另一个构造函数
作用:简化多个构造函数的代码,避免重复
示例:
1
2
3
4
5
6
7
8class Example {
public:
Example(int x): m_x(x) {}
Example(int x, int y): Example(x), m_y(y) {} // 委托构造函数和成员初始化列表不能同时存在
Example(int x, int y): Example(x) { this->m_y = y; } // ok
private:
int m_x, m_y;
};
行为:
- 减少重复代码:委托构造函数可以减少在多个构造函数中重复相同的初始化代码。这有助于提高代码的可维护性和一致性
- 初始化顺序:在委托构造函数中,首先执行被调用的构造函数,然后才执行调用构造函数的主体部分
- 限制:不能在同一个构造函数中同时使用委托构造函数和成员初始化列表
- 使用场景:适合于那些有多个构造函数,且这些构造函数之间有共同初始化逻辑
显式构造函数
定义:使用
explicit
关键字标记的构造函数作用:防止隐式类型转换和意外的构造函数调用
示例:
1
2
3
4class Example {
public:
explicit Example(...) { /* 构造函数体 */ }
};
隐式构造函数
- 定义:未用
explicit
关键字标记的构造函数 - 作用:允许编译器在需要时自动调用该构造函数进行类型转换
删除的构造函数(C++11引入)
定义:使用
delete
关键字明确禁止的构造函数作用:防止生成默认的构造函数或禁止某些类型的对象构造
示例:
1
2
3
4class Example {
public:
Example() = delete;
};
继承的构造函数(C++11引入)
定义:子类可以使用
using
语句继承基类的构造函数作用:允许子类继承基类的构造函数,简化代码
示例:
1
2
3
4
5
6
7class Base {
public:
Base(...) {}
};
class Derived: public Base {
using Base::Base;
}
行为:
- 自动转发参数:当使用基类的构造函数创建派生类对象时,构造函数的参数会被自动转发给基类的对应构造函数
不支持函数重载
关于委托构造函数和直接初始化的一些细节:
在需要代码重用和保持初始化逻辑一致时有限考虑委托构造函数
构造函数初始化逻辑和独立且简单时优先考虑直接初始化
强类型枚举
传统C++中枚举类型并非安全类型,枚举类型会被视为整数(甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同)
C++11引入枚举类
1 | enum class new_enum : unsigned int { // 未指定枚举值类型时默认为int |
不能被隐式的转换为整数,同时也不能够将其与整数数字进行比较
:wink: nice code (获取枚举值)
1 | template<typename T> |