1:强类型枚举(enum class)
1:传统C++枚举的三大缺陷
传统 C++ 枚举(enum)存在三个严重的设计缺陷,长期以来一直是 bug 的来源:
- 隐式转换为整型:枚举值会自动转换为整数,可能导致意外行为
- 污染外围作用域:枚举值会泄漏到包含它的整个作用域中
- 无法指定底层类型:不能明确控制枚举使用的存储大小,不同编译器可能有不同实现
cpp
// 传统枚举的问题示例
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // 错误:Red和Green重定义,作用域污染
int main() {
Color c = Red;
int i = c; // 隐式转换为int,可能导致意外
if (c == 0) { // 可以和整数直接比较,语义模糊
// ...
}
return 0;
}
2:强类型枚举的语法和特性
C++11 引入了强类型枚举 (也称为枚举类),使用enum class或enum struct(两者完全等价)声明,完美解决了传统枚举的所有问题:
- 枚举值必须通过枚举名::枚举值的方式访问,不会污染外围作用域
- 不会隐式转换为整型,必须使用
static_cast显式转换 - 可以显式指定底层类型,保证跨平台一致性
cpp
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // 正确:作用域隔离,无重定义
// 指定底层类型
enum class SmallEnum : uint8_t { Value1, Value2 }; // 8位存储
enum class BigEnum : uint32_t { Value1, Value2 }; // 32位存储
int main() {
Color c1 = Color::Red; // 正确:必须通过枚举名访问
// Color c2 = Red; // 错误:不能直接使用枚举值
// int i = Color::Red; // 错误:不能隐式转换为int
int j = static_cast<int>(Color::Red); // 正确:显式转换
// C++20支持:引入枚举值到当前作用域
using enum Color;
Color c = Red; // 现在可以直接使用
return 0;
}
3:强枚举类型的进阶用法
1:C++20的using enum特性
课件中提到了 C++20 的using enum特性,这里补充详细说明:
using enum EnumName会将该枚举的所有枚举值引入到当前作用域- 适用于枚举值使用频繁的场景,可以简化代码
- 注意避免不同枚举的枚举值重名
cpp
enum class Direction { Left, Right, Up, Down };
void move(Direction dir) {
switch (dir) {
using enum Direction; // 引入所有枚举值
case Left: /* ... */ break;
case Right: /* ... */ break;
case Up: /* ... */ break;
case Down: /* ... */ break;
}
}
2:强类型枚举作为位掩码
强类型枚举默认不支持位运算(如|、&、^),但我们可以通过重载运算符来实现位掩码功能,这在表示标志位时非常有用:
cpp
#include <type_traits>
enum class FileAccess : uint8_t {
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2
};
// 重载|运算符
constexpr FileAccess operator|(FileAccess a, FileAccess b) {
return static_cast<FileAccess>(
static_cast<std::underlying_type_t<FileAccess>>(a) |
static_cast<std::underlying_type_t<FileAccess>>(b)
);
}
// 重载&运算符
constexpr bool operator&(FileAccess a, FileAccess b) {
return (static_cast<std::underlying_type_t<FileAccess>>(a) &
static_cast<std::underlying_type_t<FileAccess>>(b)) != 0;
}
int main() {
FileAccess access = FileAccess::Read | FileAccess::Write;
if (access & FileAccess::Read) {
// 有读权限
}
return 0;
}
3:传统和现代的对比
| 特性 | 传统枚举(enum) | 强类型枚举(enum class) |
|---|---|---|
| 作用域 | 枚举值泄漏到外围作用域 | 枚举值封装在枚举类作用域内 |
| 隐式转换 | 自动转换为整型 | 无隐式转换,必须显式转换 |
| 底层类型 | 编译器决定,不可指定 | 可以显式指定(默认 int) |
| 前向声明 | 不支持(C++11 起支持指定底层类型) | 支持前向声明 |
| 类型安全性 | 低 | 高 |
4:扩展
- 优先使用 enum class 替代传统 enum:除非有特殊原因,否则所有新代码都应该使用强类型枚举
- 显式指定底层类型 :对于需要跨平台的代码,显式指定底层类型(如
uint8_t、uint32_t) - 避免使用匿名枚举:匿名枚举会严重污染作用域,应该用命名枚举或 constexpr 常量替代
- 不要过度使用显式转换:如果需要频繁将强类型枚举转换为整数,可能说明设计有问题
2:static_assert
1:编译期断言
static_assert是 C++11 引入的编译时断言机制,它允许开发者在编译期间检查条件是否满足,如果条件不满足,则会导致编译错误并显示指定的错误消息。
基本用法:
cpp
static_assert(常量表达式, 错误消息);
- 常量表达式:必须是在编译时可求值的表达式,结果转换为 bool 类型
- 错误消息:当断言失败时显示的字符串字面量(C++17 起可以省略)
cpp
// 1. 类型检查
template<typename T>
void process(T value) {
static_assert(std::is_integral<T>::value, "T must be an integral type");
// 函数实现...
}
// 2. 编译时常量验证
constexpr int buffer_size = 1024;
static_assert(buffer_size > 0, "Buffer size must be positive");
static_assert(buffer_size % 4 == 0, "Buffer size must be divisible by 4");
// 3. 平台或架构检查
static_assert(sizeof(void*) == 8, "This code requires 64-bit platform");
// 4. 类型大小验证
static_assert(sizeof(int) == 4, "int must be 4 bytes");
2:assert和static_assert的对比
| 特性 | static_assert | assert |
|---|---|---|
| 检查时机 | 编译时 | 运行时 |
| 影响 | 编译错误,程序无法生成 | 程序终止,输出错误信息 |
| 表达式要求 | 必须为编译时常量表达式 | 任何表达式 |
| 发布版本 | 始终生效 | 可以通过 NDEBUG 宏禁用 |
| 用途 | 类型检查、编译时常量验证、平台检查 | 运行时逻辑验证、调试 |
关键区别 :static_assert在编译时就会检查错误,而assert只有在运行时才会检查。对于可以在编译时确定的错误,应该优先使用static_assert。
3:static_assert的进阶用法
1:C++17省略错误消息
C++17 起,static_assert的错误消息参数可以省略,此时编译器会显示默认的错误消息:
cpp
static_assert(std::is_integral_v<int>); // C++17及以上支持
2:结合类型萃取进行复杂类型检查
实际上static_assert可以结合<type_traits>中的类型萃取工具进行非常复杂的类型检查:
cpp
#include <type_traits>
// 检查T是否是可复制构造且可移动构造的类型
template<typename T>
void func(T t) {
static_assert(std::is_copy_constructible_v<T>, "T must be copy constructible");
static_assert(std::is_move_constructible_v<T>, "T must be move constructible");
static_assert(!std::is_pointer_v<T>, "T must not be a pointer type");
}
// 检查类是否有特定的成员函数
template<typename T>
class MyClass {
static_assert(std::is_member_function_pointer_v<decltype(&T::toString)>,
"T must have a toString() member function");
};
3:static_assert在模版元编程的应用
static_assert是模板元编程中最常用的工具之一,它可以在编译时捕获模板参数的错误,避免生成无效的代码:
cpp
// 计算阶乘,检查输入是否为非负数
template<unsigned int N>
struct Factorial {
static_assert(N >= 0, "N must be non-negative");
static constexpr unsigned int value = N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
static constexpr unsigned int value = 1;
};
int main() {
// Factorial<-1>::value; // 编译错误:N must be non-negative
return 0;
}
4:static_assert的常见误区
1:static_assert的条件必须是编译时常量
cpp
int x = 10;
// static_assert(x > 5, "x must be greater than 5"); // 错误:x是运行时变量
2:static_assert在模版中的实例化时机
static_assert只有在模板被实例化时才会被检查。如果模板从未被实例化,即使static_assert的条件为 false,也不会导致编译错误。
3:不要用static_assert代替运行时检查
对于只能在运行时确定的条件(如用户输入、文件内容),应该使用assert或异常处理,而不是static_assert。
3:std::tuple
1:元组的基本概念
std::tuple是 C++11 引入的一个模板类,它允许将多个不同类型的值组合成一个单一的对象 。类似于结构体,但不需要预先定义类型名称。tuple 是std::pair的泛化版本,pair 只能保存两个元素,而 tuple 可以保存任意数量的元素。
tuple 是一个固定大小的异构值集合,一旦创建,大小就不能改变,每个元素的类型在编译时就确定了。
2:创建Tuple
- 直接构造
- 使用
std::make_tuple自动推导类型 - C++17 类模板参数推导(CTAD)
cpp
#include <tuple>
int main() {
// 1. 直接构造,显式指定类型
std::tuple<int, double, std::string> t1(10, 3.14, "hello");
// 2. 使用make_tuple自动推导类型
auto t2 = std::make_tuple(20, 2.718, "world");
// 3. C++17起可以使用类模板参数推导
std::tuple t3(30, 1.618, "cpp"); // 自动推导为tuple<int, double, const char*>
return 0;
}
3:访问Tuple元素
- 通过索引 访问:
std::get<N>(tuple)(N 从 0 开始) - 通过类型 访问:
std::get<T>(tuple)(C++14 起,要求类型唯一)
cpp
#include <tuple>
#include <iostream>
int main() {
std::tuple<int, double, std::string> t1(10, 3.14, "hello");
// 通过索引访问
std::cout << std::get<0>(t1) << std::endl; // 输出10
std::cout << std::get<1>(t1) << std::endl; // 输出3.14
std::cout << std::get<2>(t1) << std::endl << std::endl; // 输出"hello"
// 修改元素
std::get<0>(t1) = 100; // 修改第一个元素
// C++14起可以通过类型访问(类型必须唯一)
std::cout << std::get<int>(t1) << std::endl; // 输出100
std::cout << std::get<double>(t1) << std::endl; // 输出3.14
return 0;
}
4:解包Tuple
- 使用
std::tie解包(C++11) - 使用结构化绑定解包(C++17)
cpp
#include <tuple>
#include <iostream>
int main() {
std::tuple<int, double, std::string> t1(10, 3.14, "hello");
int x;
double y;
std::string z;
// 1. 使用std::tie解包
std::tie(x, y, z) = t1;
std::cout << x << ", " << y << ", " << z << std::endl;
// 2. C++17结构化绑定(推荐)
auto [a, b, c] = t1;
std::cout << a << ", " << b << ", " << c << std::endl;
return 0;
}
5:tuple的高级用法
1:std::tuple_cat
std::tuple_cat函数可以将多个 tuple 拼接成一个新的 tuple:
cpp
#include <tuple>
int main() {
auto t1 = std::make_tuple(1, 2);
auto t2 = std::make_tuple(3.14, "hello");
auto t3 = std::make_tuple(true);
auto t4 = std::tuple_cat(t1, t2, t3);
// t4的类型是std::tuple<int, int, double, const char*, bool>
return 0;
}
2:比较tuple
tuple 支持所有比较运算符(==、!=、<、<=、>、>=),比较是按元素顺序进行的:
cpp
#include <tuple>
#include <iostream>
int main() {
auto t1 = std::make_tuple(1, 2, 3);
auto t2 = std::make_tuple(1, 2, 4);
auto t3 = std::make_tuple(1, 3, 0);
std::cout << (t1 < t2) << std::endl; // 输出1(true)
std::cout << (t1 < t3) << std::endl; // 输出1(true)
std::cout << (t2 == t3) << std::endl; // 输出0(false)
return 0;
}
3:遍历tuple
遍历 tuple 是一个常见的需求,C++17 及以上版本可以使用折叠表达式和std::apply来实现:
cpp
#include <tuple>
#include <iostream>
#include <utility>
// C++17折叠表达式遍历
template<typename... Args>
void print_tuple(const std::tuple<Args...>& t) {
std::apply([](const auto&... args) {
((std::cout << args << " "), ...);
}, t);
std::cout << std::endl;
}
int main() {
auto t = std::make_tuple(1, 3.14, "hello", true);
print_tuple(t); // 输出:1 3.14 hello 1
return 0;
}
4:tuple作为函数返回值
tuple 最常用的场景之一是作为函数的多返回值,替代传统的输出参数:
cpp
#include <tuple>
#include <string>
// 返回多个值:姓名、年龄、分数
std::tuple<std::string, int, double> getStudentInfo(int id) {
if (id == 1) {
return {"Alice", 18, 95.5};
} else {
return {"Bob", 20, 88.0};
}
}
int main() {
auto [name, age, score] = getStudentInfo(1);
// 使用name、age、score
return 0;
}
6:tuple和struct的选择
| 场景 | 推荐使用 tuple | 推荐使用 struct |
|---|---|---|
| 临时组合少量不同类型的值 | ✅ | ❌ |
| 函数返回多个值 | ✅ | ❌(除非返回值有明确的语义关联) |
| 数据需要在多个函数之间传递 | ❌ | ✅ |
| 数据有明确的语义含义(如用户、订单) | ❌ | ✅ |
| 需要频繁访问数据成员 | ❌ | ✅(成员名比索引更清晰) |
| 需要添加成员函数 | ❌ | ✅ |
最佳实践:如果返回值超过 3 个,或者返回值有明确的语义含义,应该优先使用 struct 而不是 tuple。
4:总结
- 强类型枚举(enum class):解决了传统枚举的作用域污染和隐式转换问题,提供了更好的类型安全性
- static_assert:编译期断言机制,可以在编译时捕获类型错误和常量错误
- std::tuple:异构值集合,可以将多个不同类型的值组合成一个对象,常用于函数多返回值