1 限域enum

用法

1
enum class 枚举名 {...};

1.1 优点

  1. 避免枚举名泄露
1
2
3
4
5
6
7
8
9
10
11
//不限域
enum Color {black, white, red};
auto white = false; //错误,white在当前作用域被定义过

//限域
enum class Color {black, white, red}; //black,white,red只在Color域内
auto white = false; //没错
Color c = white //错误,此时域内没有枚举名叫white
//此时需要这样定义枚举
Color c = Color::white;
auto c = Color::white;
  1. 限域enum在其作用域中是强类型,而不限域的enum会隐式转换为整型甚至浮点型导致扭曲的效果。
1
2
3
4
5
6
7
8
enum Color { black, white, red }; 
primeFactors(std::size_t x);
Color c = red;

if (c < 14.5) { //如果是限域,c不能和double类型比较
auto factors = primeFactors(c); //也不能向参数为std::size_t的函数传递参数

}
  1. 限域enum可以直接前置声明,而非限域enum需要指定底层类型(C++11以后才可以指定底层类型)才可以前置声明。
1
2
3
enum Color; //错误
enum class Color; //没问题
enum Color: std::uint32_t; //没问题,这里指定了底层类型为std::uint32_t

原理
编译器通常在确保能包含所有枚举值的前提下为enum选择一个最小的底层类型,减少内存使用;也有一些情况会优化速度,舍弃大小,则不一定选择尽可能小的底层类型。
C++98只支持enum定义,是因为编译器需要根据列出来的枚举值在使用之前为enum选择一个底层类型。而在C++11之后,可以指定底层类型,也就可以前置声明enum。
限域enum可以直接前置声明而不需要指定底层类型,是因为它会有一个默认的底层类型。

不能前置声明enum的缺点
增加编译依赖。一个头文件的定义修改时(枚举定义增加/减少枚举值),其他包含它的头文件都需要重新编译。通过enum前置声明,告诉编译器某个枚举类型的存在,之后在其他地方定义/修改,包含声明的头文件都不需要重新编译,使用这个enum的,只要没用到新添加的枚举值,也不需要重新编译。

1.2 缺点

  • 配合C++11的std::tuple使用时(std::tuple的用法是定义一个变量存储不同数据类型的元素)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//tuple一般使用,此时很难记住获取的字段代表什么含义

//假设想用tuple存储用户的信息,分别是名字、email地址、声望
using UserInfo = std::tuple<std::string, std::string, std::size_t>
UserInfo uInfo; //tuple对象
auto val = std::get<1>(uInfo); //通过这个函数获取第一个字段



//配合不限域enum使用

enum UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
//这样就可以清楚知道是获取邮箱地址字段
auto val = std::get<uiEmail>(uInfo);
//原理:std::get所需的模板实参是std::size_t类型的,而不限域enum能隐式转换为std::size_t类型



//配合限域enum使用

enum class UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);//需要写的长很多

改良方法
既避免冗长的表示,且又能使用限域enum避免命名空间污染:编写函数将限域枚举类型的值转换为std::size_t类型的值,或者直接转换为可以作为std::get模板实参的枚举的底层整数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//详解:

//枚举值类型到所需模板实参类型的转换需要在编译期实现,所以需要加constexpr

//std::underlying_type_t<E>用来获取枚举的底层数据类型,也就是要转换为的类型
//这里是C++14风格,在C++11中是std::underlying_type<E>::type

//加上noexcept标明这个函数不会产生异常

//static_cast实现转换
template<typename E>
constexpr std::underlying_type_t<E> toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

2 deleted函数

  • 使用场景1:对于成员函数,如果不想被客户端调用,可以声明为私有的;如果也不想被其他成员函数或类的友元调用,就可以不定义它们。用= delete可以直接将函数声明标记为“删除的函数”,实现以上功能,并且deleted函数一般要声明为public(一定要声明为public),这样被调用时会更清晰的报错而不是报private错误。

  • 使用场景2:相比较于private,deleted可以标记所有函数,包括普通函数。因此可以避免C++无意义的类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
//如果有这样一个函数去判定是否幸运数
bool isLucky(int number);

//传入其他非整型也能隐式转换为int,但无意义
isLucky('a');
isLucky(true);
isLucky(3.5);

//使用deleted函数避免
bool isLucky(int number); //声明原始版本
bool isLucky(char) = delete; //拒绝char
bool isLucky(bool) = delete; //拒绝bool
bool isLucky(double) = delete; //拒绝double
  • 使用场景3:禁止特定的模板实例化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//对于以下的模板函数
template<typename T>
void processPointer(T* ptr);

//如果希望拒绝void*指针或者char*指针作为参数去调用,就可以:
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;

//还可以更彻底的拒绝其他重载版本
template<>
void processPointer<const void>(const void*) = delete;
template<>
void processPointer<const char>(const char*) = delete;

3 override和final

override作用: 派生类重写基类函数时加上此关键字,以防想要重写函数,但写错了被视为派生类自己的成员函数,进而编译器无法报错提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};

class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; //可以添加virtual,但不是必要
};

final作用: 1、给虚函数添加final可以防止派生类重写。2、final用于类,则此类无法作为基类。

其他:这两个关键字只在特定上下文才被视为关键字。例:override只在成员函数结尾处才被视为关键字。

1
2
3
4
5
6
class Warning {         //C++98潜在的传统类代码
public:

void override(); //C++98和C++11都合法(且含义相同)

};

4 引用限定

作用: 可以限定成员函数只能用于左值或者右值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Widget {
public:

void doWork() &; //只有*this为左值的时候才能被调用
void doWork() &&; //只有*this为右值的时候才能被调用

int& getValue()
{
return value; //返回成员变量的引用,是左值
}

int value;
};

Widget makeWidget(); //工厂函数(返回右值)
Widget w; //普通对象(左值)

w.doWork(); //调用被左值引用限定修饰的Widget::doWork版本
//(即Widget::doWork &)
makeWidget().doWork(); //调用被右值引用限定修饰的Widget::doWork版本
//(即Widget::doWork &&)

//这两个getValue返回的都是左值,具体看函数声明时的返回值什么,而不是看调用的对象是左值还是右值
auto value1 = w.getValue();
auto value2 = makeWidget().getValue();

5 constexpr关键字

  • 作用:用于指定变量、函数、构造函数可以在编译时求值,大大优化性能。

5.1 constexpr变量

constexpr int max_size = 100; // 编译时常量

  • 必须初始化,且初始化表达式必须是一个常量表达式。
1
2
3
4
5
6
7
8
int sz;                             //non-constexpr变量

constexpr auto arraySize1 = sz; //错误!sz的值在编译期不可知

std::array<int, sz> data1; //错误!模板实参也需要编译器可知变量
constexpr auto arraySize2 = 10; //没问题,10是编译期可知常量

std::array<int, arraySize2> data2; //没问题, arraySize2是constexpr

5.2 constexpr函数

当传递的是编译期可知的值时,constexpr函数可以产出编译期可知的值。传递的值是运行时可知的,constexpr产出的值就也是运行时才可知。

1
2
3
4
5
constexpr int square(int x) { return x * x; } 
int main() {
constexpr int result = square(5); // 编译时计算
return 0;
}

5.3 constexpr构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Point {  
public:
constexpr Point(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}
constexpr double getX() const { return x; }
constexpr double getY() const { return y; }

private:
double x, y;
};

//初始化类对象
constexpr Point p(1.0, 2.0); // 编译时初始化
  • 类对象必须是constconstexpr
  • 初始化表达式必须是一个常量表达式

5.4 if constexpr

  • 作用:可以在编译时根据常量表达式决定执行的代码块

  • 基本语法:

1
2
3
4
5
if constexpr (常量表达式) {  
// 当常量表达式为 true 时编译这部分代码
} else {
// 当常量表达式为 false 时编译这部分代码(可选)
}
  • 实例:根据模板的类型来决定执行哪块代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>  
#include <type_traits>

template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Processing integer: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Processing floating point: " << value << std::endl;
} else {
std::cout << "Processing some other type" << std::endl;
}
}

int main() {
process(42); // 输出: Processing integer: 42
process(3.14); // 输出: Processing floating point: 3.14
process(std::string("x"));// 输出: Processing some other type
return 0;
}

5.5 constexpr和const的关系

所有constexpr对象都是const,但不是所有const对象都是constexpr。

1
2
3
int sz;
const auto arraySize = sz; //没问题,arraySize是sz的const复制
std::array<int, arraySize> data; //错误,arraySize值在编译器不可知

6 noexcept

6.1 noexcept声明的方式

1
2
3
4
5
6
7
8
9
10
11
// 函数声明
void function_name() noexcept;
// 函数定义
void function_name() noexcept { /* ... */ }
//用于 lambda 表达式
auto lambda = []() noexcept {
// lambda 体
};

//根据编译期常量判断函数是否为 noexcept
void function_name() noexcept(is_noexcept); // 其中 is_noexcept 是一个编译时常量表达式

6.2 声明noexcept的好处和作用

性能优化:

编译器可以对 noexcept 函数进行额外的优化。例如,在某些情况下,编译器可以省略设置异常处理代码(如栈展开所需的元数据),从而提高程序的执行效率。

在移动操作中,如果一个类型声明了其移动构造函数或移动赋值操作符为 noexcept,那么标准库容器(如 std::vector)在需要重新分配内存时会优先使用移动而非拷贝,这通常能带来显著的性能提升。

异常安全性:

避免栈展开:当异常抛出时,如果函数被声明为 noexcept,程序会调用 std::terminate()以未定义的方式终止,避免了栈展开过程,这有助于减少程序崩溃的风险。

接口清晰性:

明确地声明一个函数为 noexcept 可以使接口更加清晰,使用者知道这个函数不会抛出异常,因此不需要为其编写异常处理代码。这也使得代码的意图更加明确,增强了可读性和维护性。

标准库兼容性:

标准库中的某些功能依赖于 noexcept 保证。例如,std::move_if_noexcept 会根据移动构造函数是否被声明为 noexcept 来决定是使用移动还是拷贝。如果移动操作被声明为 noexcept,则优先使用移动;否则使用拷贝。

7 const_iterator

  • 对于迭代器使用上优先使用 const_iterator, 而非 iterator

const_iterator 是表示迭代器指向的对象是一个常量,相当于常量指针(左const),指针指向的对象是常量; 而 const iterator 是表示迭代器本身是一个常量,相当于指针常量(右const),指针本身是一个常量,指针本身不可修改,但是指向的对象,该对象可以修改。

8 特殊成员函数的生成

  1. C++11后会自动生成的特殊函数:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。

  2. 两个拷贝操作是独立的:声明一个不会限制编译器生成另一个,例如声明了拷贝构造函数,但没有声明拷贝赋值运算符,但写的代码用到了拷贝赋值,编译器就会生成拷贝赋值运算符;两个移动操作不是相互独立:如果声明了其中一个,编译器就不再生成另一个。

  3. 对于除默认构造函数之外的五个特殊函数在C++11之后有0/3/5原则:

    1. 一个也不声明。
    2. 两个拷贝和析构,声明了其中一个就要声明另外两个。
    3. 两个拷贝两个移动和析构全部声明。
  4. 当下面条件成立时才会生成移动操作

    1. 类中没有拷贝操作
    2. 类中没有移动操作
    3. 类中没有用户定义的析构
  5. 对于编译器可能不会生成的特殊函数,default关键字总能强制编译器生成这些函数的默认实现(不需要定义)。

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
virtual ~Base() = default; //使析构函数virtual

Base(Base&&) = default; //支持移动
Base& operator=(Base&&) = default;

Base(const Base&) = default; //支持拷贝
Base& operator=(const Base&) = default;

};
  1. 其他自动生成规则:
    1. 默认构造函数:当类不存在用户声明的构造函数时才自动生成。
    2. 析构函数:当基类析构为虚函数时,改类析构才为虚函数。
    3. 拷贝构造函数:当类没有用户定义的拷贝构造时才生成,如果类声明了移动操作,它就是delete的。
    4. 拷贝赋值运算符:没有用户定义的拷贝赋值时才生成,如果类声明了移动操作,它就是delete的。