超市系统 V2.0:精细化运营

1. 商品类别与类型安全——enum classusing enum

需求:超市商品种类繁多,需要精确分类(食品、电器、服装等)。之前用整数或字符串表示类别,容易混淆,比如误将电器类别传入食品函数。我们需要强类型枚举。

解决方案:C++11的enum class

1
2
3
4
5
6
enum class Category { Food, Electronics, Clothing, /*...*/ };

class Product {
Category category;
// ...
};

enum class不会隐式转换为整数,避免了意外混用。C++20引入了using enum,可以在作用域内引入枚举成员,简化代码:

1
2
3
4
5
6
7
void processProduct(const Product& p) {
using enum Category; // 引入 Food, Electronics 等
switch (p.category) {
case Food: /*...*/ break;
case Electronics: /*...*/ break;
}
}

原理enum class是强作用域枚举,底层类型默认为int但可指定(如enum class Category : uint8_t)。编译器会检查类型匹配,生成代码时实际还是整数,但增加了类型安全。

2. 商品初始化——统一初始化与 initializer_list

需求:创建商品对象时,我们希望用简洁的语法初始化,并防止窄化转换(比如用浮点数初始化整型价格)。

解决方案:C++11统一初始化(花括号初始化)。

1
2
3
Product p{"牛奶", 5.5, "6901234567890", Category::Food}; // 没问题
Product p2{"牛奶", 5.5f, "6901234567890", Category::Food}; // 浮点转double安全
// Product p3{"牛奶", 5.5, "6901234567890", 0}; // 错误:不能将int转为Category

花括号初始化禁止窄化转换,比如int x{3.14}会编译错误。同时,如果类有接受std::initializer_list的构造函数,花括号会优先匹配它,可用于初始化容器或自定义列表。

原理std::initializer_list是一个轻量级代理对象,编译器会构造一个数组并将首地址和长度传给构造函数。它通常用于构造函数或赋值运算符。

3. 商品可选属性——std::optional

需求:并非所有商品都有保质期(比如电器),也并非所有商品都有折扣。如果用特殊值(如-1)或空指针表示“无”,容易出错。我们希望类型系统明确表达“可能有值,也可能没有”。

解决方案:C++17的std::optional

1
2
3
4
5
6
7
8
9
class Product {
std::optional<Date> expiryDate; // 可能无保质期
std::optional<double> discount; // 可能无折扣
public:
bool isExpired() const {
if (!expiryDate) return false; // 无保质期视为永不过期
return *expiryDate < Date::today();
}
};

原理std::optional<T>内部包含一个bool标志和一个union(或对齐的存储空间)来放置T。它不分配动态内存,只是原地存储。访问时用*value(),如果无值且调用value()会抛std::bad_optional_access。它的开销仅是一个布尔值和内存对齐要求,非常高效。

4. 多种商品类型存储——std::variant

需求:我们有不同种类的商品,虽然可以用继承,但类型固定且数量有限,继承会引入虚表开销。我们想存储不同类型的商品,并能安全地访问它们。

解决方案:C++17的std::variant( tagged union)。

1
2
3
4
5
6
7
8
9
using ProductVariant = std::variant<Food, Electronics, Clothing>;

std::vector<ProductVariant> inventory;

void printInfo(const ProductVariant& pv) {
std::visit([](auto&& product) {
std::cout << product.name << " 类别: " << product.category << std::endl;
}, pv);
}

std::visit会根据当前存储的类型调用对应的lambda。如果某个类型没有处理,编译错误。variant保证类型安全,不会出现未定义行为。

原理std::variant内部是一个足够大的缓冲区(最大类型的大小),加上一个索引表示当前激活的类型。它使用类型安全联合,构造和析构时正确调用类型的构造/析构。std::visit通过编译期生成函数表(或一系列if constexpr)实现访问,没有虚函数开销,但可能涉及多次判断。

5. 字符串视图——std::string_view

需求:在查找商品、打印日志时,经常传递只读字符串(如商品名称)。如果每次都拷贝std::string,会有不必要的内存分配。我们希望一个轻量级的字符串视图。

解决方案:C++17的std::string_view

1
2
3
4
void findProduct(std::string_view name) { // 不拷贝,只引用
auto it = inventory.find(name); // 如果map的key是string,需要转换
// ...
}

注意string_view不拥有字符串,生命周期必须长于使用它的地方。如果底层字符串被销毁,视图悬挂。

原理string_view通常包含两个成员:const char*size_t,大小只有两个指针,拷贝极快。它提供类似string的只读接口,但不涉及内存管理。C++17还提供了string_view字面量后缀sv

6. 数组视图——std::span

需求:批量处理商品时,我们经常操作连续的商品数组(如收银台的购物车)。传指针和长度容易出错,我们希望一个轻量级的视图。

解决方案:C++20的std::span

1
2
3
4
5
void calculateTotal(std::span<const Product> cart) {
double total = 0;
for (const auto& p : cart) total += p.price;
// ...
}

span可以接受数组、std::vectorstd::array等,且不拥有数据。它包含指针和长度,类似string_view但针对任意类型。

原理span<T>T*size_t的封装,提供边界安全(如果开启编译选项),但不做越界检查(除非用.at())。它支持动态长度或静态长度(span<T, N>),静态长度在编译期已知,可优化。

7. 三向比较(飞船运算符)——<=>

需求:商品需要排序,比如按价格、按名称。C++20前我们需要定义六个比较运算符,很繁琐。我们希望编译器自动生成。

解决方案:C++20的三向比较运算符<=>

1
2
3
4
5
6
7
8
9
class Product {
std::string name;
double price;
public:
auto operator<=>(const Product&) const = default; // 默认按所有成员比较
// 如果默认行为不符合,可以自定义
};

// 现在可以直接使用 <, <=, >, >=, ==, !=

原理<=>返回一个比较类别类型(std::strong_ordering等),表示小于、等于、大于或无序。编译器会根据返回类型自动生成其他比较运算符。如果成员都有<=>,且返回类型一致,则=default可用。如果类型中有浮点数,则返回std::partial_ordering(因为NaN)。自定义<=>可以精细控制比较逻辑,且性能优于逐个比较。

8. 编译期断言——static_assert

需求:模板编程中,我们希望确保某些条件在编译期成立,否则报错。例如,要求价格不能为负数,或者要求某些类型必须满足某些特性。

解决方案static_assert

1
2
3
4
5
6
template<typename T>
class DiscountCalculator {
static_assert(std::is_floating_point_v<T> || std::is_integral_v<T>,
"DiscountCalculator only works with arithmetic types");
// ...
};

C++17起static_assert可以不提供消息字符串,简化使用。

原理static_assert在编译期求值,如果条件为false,编译器输出错误信息并终止编译。它常用于约束模板参数,验证类型特性。

9. 类型别名与decltype(auto)

需求:在复杂模板中,我们需要推导表达式的类型,并且希望保留引用性。例如,编写一个函数返回容器中元素的引用。

解决方案:C++11的decltypeauto,C++14的decltype(auto)

1
2
3
4
template<typename Container>
decltype(auto) getFirst(Container&& c) {
return std::forward<Container>(c)[0]; // 返回引用或值取决于c
}

decltype(auto)根据decltype规则推导类型,保留引用和cv限定符。如果返回c[0]c是左值容器,则返回左值引用;如果是右值容器,可能返回右值引用或值(取决于operator[])。

原理auto推导会忽略引用和顶层const(除非显式auto&),而decltype(expression)会精确推导表达式类型。decltype(auto)decltype规则应用于初始化表达式。

10. 结构化绑定

需求:从std::map中遍历商品时,我们得到pair,每次写.first.second很啰嗦。我们希望直接解包成变量。

解决方案:C++17结构化绑定。

1
2
3
4
std::map<std::string, Product> productMap;
for (const auto& [barcode, product] : productMap) {
std::cout << barcode << ": " << product.name << std::endl;
}

结构化绑定可以用于数组、pairtuple、结构体等。原理是编译器生成匿名变量,然后将成员绑定到对应名字。

11. 广义lambda捕获

需求:在异步任务中,我们需要将unique_ptr传递给lambda,但按值捕获unique_ptr不可拷贝,按引用捕获可能悬挂。我们希望移动捕获。

解决方案:C++14的广义lambda捕获(init-capture)。

1
2
3
4
auto task = [p = std::make_unique<Product>(...)]() {
// 使用 p
};
std::thread t(std::move(task)); // lambda可移动

这里p是lambda的成员变量,通过std::move初始化。原理是编译器生成一个类,成员通过初始化表达式构造,支持移动。

12. 属性——[[nodiscard]], [[maybe_unused]]

需求:有些函数返回值必须处理,比如检查库存是否成功;有些参数可能暂时不用,但不想编译器警告。

解决方案:C++17/20引入的属性。

1
2
3
4
5
6
7
8
9
10
11
[[nodiscard]] bool deductStock(const std::string& barcode, int quantity);

void process() {
deductStock("123", 1); // 编译器警告:返回值被忽略
}

void log([[maybe_unused]] const std::string& msg) { // 未使用参数,不警告
#ifdef DEBUG
std::cout << msg;
#endif
}

C++20还增加了[[likely]][[unlikely]],用于分支预测优化。

1
2
3
if (isExpired) [[unlikely]] {
handleExpired();
}

原理:属性是编译器提示,不改变语义但可优化或产生警告。[[nodiscard]]会促使调用者检查返回值。

13. 自定义字面量

需求:价格经常用小数表示,我们希望直接用99.99_元这样的字面量,增强可读性。

解决方案:C++11允许自定义字面量后缀。

1
2
3
4
constexpr long double operator"" _元(long double price) { return price; }
constexpr long double operator"" _元(unsigned long long price) { return price; }

double total = 99.99_元 + 20_元;

可以定义整型、浮点型、字符串、字符字面量。原理是编译器将字面量转换为对运算符函数的调用。

14. Pimpl惯用法

需求:商品类的头文件包含了太多内部实现细节,导致依赖过多,编译慢。我们希望隐藏实现,减少编译耦合。

解决方案:Pimpl(Pointer to Implementation)惯用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Product.h
class Product {
public:
Product();
~Product();
// 公开接口
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};

// Product.cpp
struct Product::Impl {
std::string name;
double price;
// ...
};

Product::Product() : pImpl(std::make_unique<Impl>()) {}
Product::~Product() = default; // 需要可见的Impl定义

原理:Pimpl将实现放在源文件,通过指向前向声明类的指针访问。这样修改实现不会触发重新编译客户代码。使用unique_ptr管理生命周期,注意析构函数需要在Impl定义处实现(或提供自定义删除器)。移动操作也需要显式定义。

15. CRTP(奇异递归模板模式)

需求:不同商品有不同的折扣算法,如果用虚函数,每个商品都要有虚表,有开销。我们想在编译期绑定行为,实现静态多态。

解决方案:CRTP。

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Derived>
class Discountable {
public:
double getDiscount() const {
return static_cast<const Derived*>(this)->getDiscountImpl();
}
};

class Food : public Discountable<Food> {
public:
double getDiscountImpl() const { /* 食品折扣 */ }
};

使用时,通过基类引用调用getDiscount,但实际是编译期绑定,无虚函数开销。CRTP常用于实现代码复用,如Boost.Operators。

原理:派生类将自身作为模板参数传给基类,基类可以通过static_cast获得派生类对象,从而调用其成员。这本质上是一种编译期多态。

16. 虚继承与菱形继承

需求:如果商品类型有更复杂的层次,比如“进口食品”同时继承“食品”和“进口商品”,可能形成菱形继承,导致二义性和冗余子对象。

解决方案:虚继承。

1
2
3
4
class Product { /*...*/ };
class Food : public virtual Product { /*...*/ };
class Imported : public virtual Product { /*...*/ };
class ImportedFood : public Food, public Imported { /*...*/ };

虚继承确保Product子对象只有一份,避免二义性。代价是访问虚基类成员需要间接寻址(通过虚基类指针),增加开销。构造顺序也有特殊规则。

原理:虚继承的类内部有一个或多个虚基类指针(vptr-like),指向虚基类子对象偏移量。内存布局比普通继承复杂。

17. 友元

需求:我们有一个全局函数printReceipt,需要访问商品类的私有成员(如折扣价),但不希望将其作为成员函数。

解决方案:友元。

1
2
3
4
5
6
7
8
9
class Product {
friend void printReceipt(const Product& p);
private:
double actualPrice() const;
};

void printReceipt(const Product& p) {
std::cout << "实付: " << p.actualPrice(); // 可以访问私有成员
}

友元可以是一个函数、另一个类或成员函数。友元关系不可传递,且破坏封装,应谨慎使用。

18. explicit与转换构造函数

需求:防止隐式转换带来的意外,比如用double隐式构造Price类,可能导致混淆。

解决方案explicit关键字。

1
2
3
4
5
6
7
8
9
class Price {
public:
explicit Price(double value) : value_(value) {}
// ...
};

void checkout(Price p);
checkout(9.99); // 错误:不能隐式转换
checkout(Price{9.99}); // 正确

C++11允许explicit用于转换运算符,如explicit operator bool(),避免隐式布尔转换。

原理explicit构造函数只允许直接初始化,不允许拷贝初始化(=),也不允许隐式转换。

19. mutable与逻辑常量性

需求:在const成员函数中,我们可能需要修改一些缓存数据,比如计算折扣后的价格缓存,但不改变对象逻辑状态。

解决方案mutable关键字。

1
2
3
4
5
6
7
8
9
10
11
12
class Product {
mutable double cachedDiscount{0.0};
mutable bool discountValid{false};
public:
double getDiscount() const {
if (!discountValid) {
cachedDiscount = computeDiscount(); // 修改 mutable 成员
discountValid = true;
}
return cachedDiscount;
}
};

mutable成员即使在const对象中也可以修改,常用于线程安全中的互斥锁、缓存等。

20. 构造函数委托

需求:一个类有多个构造函数,它们有重复代码,我们希望一个构造函数调用另一个。

解决方案:C++11构造函数委托。

1
2
3
4
5
class Product {
public:
Product() : Product("未知", 0.0) {} // 委托给另一个构造
Product(std::string name, double price) : name_(std::move(name)), price_(price) {}
};

注意避免循环委托。委托构造的初始化列表只能包含对其他构造的调用,不能同时有其他成员初始化。

21. 继承构造函数

需求:派生类希望直接使用基类的构造函数,而不需要一一转发参数。

解决方案:C++11的继承构造函数(using声明)。

1
2
3
4
class DiscountedProduct : public Product {
using Product::Product; // 继承所有基类构造函数
double discountRate;
};

这会生成对应的转发构造函数,但不会初始化派生类成员(它们会默认初始化)。C++17允许继承构造函数的属性。

22. finaloverride

需求:防止类被继承,或防止虚函数被重写,确保设计意图。

解决方案:C++11的finaloverride

1
2
3
4
5
6
7
8
9
10
11
class Product final { // 不能被继承
// ...
};

class Food : public Product { // 错误
};

class SpecialProduct : public Product {
void someMethod() override; // 显式声明重写,如果基类没有虚函数则报错
void dontOverride() final; // 后续派生类不能重写
};

override帮助编译器检查是否真的重写了基类虚函数,避免拼写错误。

23. 成员指针

需求:有时我们需要指定按对象的哪个成员排序,比如按价格或按名称,写两个比较函数很冗余。成员指针可以让我们传递成员。

解决方案:指向成员的指针。

1
2
3
4
5
6
7
8
9
template<typename T, typename U>
auto sortBy(std::vector<T>& vec, U T::* member) {
std::sort(vec.begin(), vec.end(), [member](const T& a, const T& b) {
return a.*member < b.*member;
});
}

sortBy(products, &Product::price); // 按价格排序
sortBy(products, &Product::name); // 按名称排序

成员指针U T::*表示指向T类中类型为U的成员的指针。访问时用.*->*。成员函数指针类似,但更复杂。

24. 位域

需求:商品有许多标志位(是否促销、是否会员专享、是否进口),每个只占1位。我们希望节省内存。

解决方案:位域。

1
2
3
4
5
6
struct ProductFlags {
bool isOnSale : 1;
bool isMemberOnly : 1;
bool isImported : 1;
// 可能还有更多
};

位域可以指定比特位数,多个标志可压缩在一个整型中。但位域的布局由编译器决定,不可移植,且不能取地址。

25. 联合体(union

需求:某些商品有特殊属性,比如电器有电压,服装有尺码,这些属性互斥,我们希望节省内存。

解决方案union,但更安全的做法是用std::variant。不过了解union仍有必要。

1
2
3
4
5
6
union ProductAttribute {
int voltage; // 电器
std::string size; // 服装 // C++11允许非平凡类型,但需手动管理
ProductAttribute() {}
~ProductAttribute() {}
};

C++11后union可以包含有非平凡特殊成员函数的类型,但需要手动管理构造和析构,极易出错。因此实践中优先用variant

26. std::any

需求:促销规则可能附加任意类型的参数,比如满减需要金额,打折需要折扣率。我们想存储任意类型的值。

解决方案:C++17的std::any

1
2
3
4
5
6
7
8
class Promotion {
std::any param;
public:
template<typename T>
void setParam(T value) { param = value; }
template<typename T>
T getParam() const { return std::any_cast<T>(param); }
};

原理std::any内部通过类型擦除存储任意类型,通常包含一个虚表指针和分配的内存(小对象可能用内部缓冲区)。访问需要any_cast,如果类型不匹配会抛异常。

27. 时间处理——std::chrono

需求:超市需要根据时间段打折,比如晚上8点后生鲜8折。需要精确计时,并且测量促销活动的性能。

解决方案std::chrono库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std::chrono;
auto now = system_clock::now();
auto today = floor<days>(now);
auto time_since_midnight = now - today;
auto hour = duration_cast<hours>(time_since_midnight).count();

if (hour >= 20) {
applyDiscount();
}

// 性能测量
auto start = steady_clock::now();
// 执行任务
auto end = steady_clock::now();
auto elapsed = duration_cast<microseconds>(end - start);

原理chrono提供三种时钟:system_clock(系统时间,可转成日历),steady_clock(单调时钟,适合测量),high_resolution_clock(通常是steady_clock别名)。时间点、时间段、时钟都是强类型,防止单位混淆。C++20还添加了日历和时区支持。

28. 随机数——std::random

需求:模拟顾客购物行为,比如随机生成购买商品的数量;或者随机抽奖。

解决方案std::random库。

1
2
3
4
5
std::random_device rd;  // 用于种子
std::mt19937 gen(rd()); // 梅森旋转算法
std::uniform_int_distribution<> dis(1, 10); // 1到10均匀分布

int quantity = dis(gen); // 随机数量

原理:C++11提供了多种随机数引擎和分布,如normal_distribution等,比C的rand()质量更高,可重复性好。

29. 正则表达式——std::regex

需求:验证商品条码格式(例如EAN-13),提取其中的信息。

解决方案std::regex

1
2
3
4
5
6
7
8
9
10
std::regex barcode_pattern(R"(\d{13})"); // 13位数字
if (!std::regex_match(barcode, barcode_pattern)) {
throw std::invalid_argument("条码格式错误");
}

// 提取前三位国家码
std::smatch match;
if (std::regex_search(barcode, match, std::regex(R"(^(\d{3})"))) {
std::string country_code = match[1];
}

原理std::regex使用ECMAScript语法(默认),支持多种正则引擎实现(如DFA、NFA)。但注意,不同编译器的实现可能有性能差异,且不是所有都完全支持ECMAScript。C++还提供std::regex_iteratorstd::regex_token_iterator遍历匹配。

30. 文件系统——std::filesystem

需求:每天备份库存数据,需要创建目录、复制文件、列出备份文件。

解决方案:C++17的std::filesystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace fs = std::filesystem;

void backupInventory(const std::string& backup_dir) {
fs::create_directories(backup_dir);
auto now = fs::file_time_type::clock::now();
auto filename = backup_dir + "/inventory_" + std::to_string(now.time_since_epoch().count()) + ".dat";
fs::copy_file("inventory.dat", filename, fs::copy_options::overwrite_existing);
}

// 列出备份
for (auto& entry : fs::directory_iterator(backup_dir)) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename() << " size: " << entry.file_size() << std::endl;
}
}

原理filesystem库是对操作系统文件操作的封装,提供可移植的路径、文件类型、权限、时间戳等操作。它使用std::error_code处理错误,避免异常开销。

31. 线程同步——条件变量、闩、屏障、信号量

需求:收银台(生产者)和打包员(消费者)之间需要协调;多个收银台同时开始营业需要同步;统计每日销售额时需要等待所有收银台完成;限制同时访问数据库的线程数。

解决方案:C++11条件变量,C++20闩(latch)、屏障(barrier)、信号量(semaphore)。

条件变量:用于生产者-消费者队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::queue<Order> orders;
std::mutex mtx;
std::condition_variable cv;

// 生产者(收银台)
void produce(Order order) {
{
std::lock_guard lk(mtx);
orders.push(order);
}
cv.notify_one();
}

// 消费者(打包员)
void consume() {
while (true) {
std::unique_lock lk(mtx);
cv.wait(lk, []{ return !orders.empty(); });
auto order = orders.front();
orders.pop();
lk.unlock();
pack(order);
}
}

wait会原子地解锁互斥并阻塞,被唤醒后重新锁定并检查条件,避免虚假唤醒。

latch:一次性同步,例如等待所有收银台准备就绪。

1
2
3
4
5
6
7
std::latch start_latch(num_checkouts);

void checkout(int id) {
// 准备工作
start_latch.arrive_and_wait(); // 等待所有收银台到达
// 同时开始收银
}

barrier:可重复使用的同步点,例如每天关店后汇总销售数据,多个线程计算各自销售额,然后在屏障处同步,主线程累加。

1
2
3
4
5
6
7
8
9
std::barrier sync_point(num_threads, []() noexcept {
std::cout << "所有线程已到达屏障,可以汇总\n";
});

void worker() {
// 计算自己的销售额
sync_point.arrive_and_wait(); // 等待所有线程
// 汇总(可能由最后一个到达的线程执行完成函数)
}

semaphore:限制并发数,比如数据库连接池。

1
2
3
4
5
6
7
std::counting_semaphore<> db_pool(10); // 最多10个并发

void query() {
db_pool.acquire(); // 计数减1,如果为0则阻塞
// 使用数据库
db_pool.release(); // 计数加1
}

C++20提供了std::counting_semaphorestd::binary_semaphore

32. 异步任务——std::future/promise/async

需求:需要异步生成销售报表,不阻塞主界面;或者从多个数据源并行获取商品信息。

解决方案std::asyncstd::futurestd::promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用async启动异步任务
auto future_report = std::async(std::launch::async, generateReport);
// 做其他事情
auto report = future_report.get(); // 等待结果

// 使用promise手动设置值
std::promise<std::vector<Product>> p;
auto f = p.get_future();
std::thread t([p = std::move(p)]() mutable {
std::vector<Product> products = fetchFromDB();
p.set_value(products);
});
auto products = f.get();
t.join();

原理future表示一个将来可用的值,promise用于设置该值。async根据策略(std::launch::async强制新线程,std::launch::deferred惰性求值)启动任务,返回future。C++20还增加了std::jthread和停止令牌。

33. 可停止线程——std::jthreadstd::stop_token

需求:后台监控线程(如检查库存过期)需要优雅停止,不能强制终止。

解决方案:C++20的jthread和停止令牌。

1
2
3
4
5
6
7
8
9
10
std::jthread monitor([](std::stop_token st) {
while (!st.stop_requested()) {
checkExpiredProducts();
std::this_thread::sleep_for(1min);
}
});

// 在主线程中
monitor.request_stop(); // 请求停止
// jthread析构时会自动join

jthread在析构时自动join,并支持中断。stop_token可轮询或与条件变量配合(condition_variable_any支持stop_token)。

34. 原子等待与通知

需求:需要轻量级线程同步,比如一个线程更新价格,另一个线程等待变化。

解决方案:C++20原子类型的waitnotify

1
2
3
4
5
6
7
8
9
10
11
12
13
std::atomic<double> price{100.0};

// 等待价格变化
void priceWatcher() {
price.wait(100.0); // 如果当前值等于100则阻塞,直到被通知
std::cout << "价格变为: " << price.load() << std::endl;
}

// 更新价格
void priceUpdater(double newPrice) {
price.store(newPrice);
price.notify_all(); // 唤醒所有等待线程
}

这比条件变量更轻量,适合单值变化。内部实现可能使用 Futex 或类似机制。

35. 原子引用——std::atomic_ref

需求:需要对一个非原子对象进行原子操作,比如多个线程操作共享的统计数据,但该对象原本不是原子类型。

解决方案:C++20的std::atomic_ref

1
2
3
4
5
6
7
8
9
10
11
12
struct Stats {
long long totalSales = 0;
int transactionCount = 0;
};

Stats stats;

void checkout(double amount) {
static_assert(std::atomic_ref<long long>::is_always_lock_free);
std::atomic_ref<long long> atomicSales(stats.totalSales);
atomicSales.fetch_add(static_cast<long long>(amount * 100)); // 原子加
}

atomic_ref临时将对象包装为原子操作,但需要确保对象生命周期内无其他非原子访问。

36. 并行算法与执行策略

需求:每天要对数十万商品进行统计,比如计算平均价格、找出最贵商品,使用单线程太慢,需要并行。

解决方案:C++17的并行算法(需要编译器支持)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <execution>
std::vector<Product> products;
// 并行计算平均价格
double avg = std::transform_reduce(std::execution::par,
products.begin(), products.end(),
0.0,
std::plus<>(),
[](const Product& p) { return p.price; }
) / products.size();

// 并行排序
std::sort(std::execution::par, products.begin(), products.end(),
[](auto& a, auto& b) { return a.price < b.price; });

执行策略std::execution::par表示并行,par_unseq还可能向量化。需要支持TBB或类似后端。并行算法内部会分割任务,利用多核。

37. 多态内存资源——std::pmr

需求:临时票据对象频繁分配,导致内存碎片和性能下降。我们希望使用内存池来管理这些对象。

解决方案:C++17的std::pmr(多态内存资源)。

1
2
3
4
5
6
7
8
9
#include <memory_resource>

// 创建一个monotonic_buffer_resource,从缓冲区分配
std::array<std::byte, 10000> buffer;
std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
std::pmr::polymorphic_allocator<Ticket> alloc{&pool};

// 使用分配器构造对象
auto ticket = std::allocate_shared<Ticket>(alloc, /*args*/);

pmr提供一组内存资源类(如monotonic_buffer_resourcesynchronized_pool_resource),和兼容STL容器的多态分配器。容器如std::pmr::vector<T>使用多态分配器。可以控制内存分配策略,提高性能。

原理std::pmr::memory_resource是一个抽象基类,allocatedeallocate是虚函数。polymorphic_allocator持有memory_resource*,将分配请求转发给它。这样容器可以使用不同的内存资源。

38. 高性能数值转换——std::to_chars/from_chars

需求:解析价格字符串(如”19.99”)需要高性能,避免std::stringstream开销。

解决方案:C++17的std::to_charsstd::from_chars

1
2
3
4
5
6
7
8
std::string price_str = "19.99";
double value;
auto [ptr, ec] = std::from_chars(price_str.data(), price_str.data() + price_str.size(), value);
if (ec == std::errc()) {
// 成功
} else {
// 错误
}

这些函数不依赖locale,无异常,最快。它们返回错误码和剩余指针。类似地,to_chars将数值转换为字符串。

39. 安全类型双关——std::bit_cast

需求:需要将double的二进制表示作为uint64_t进行哈希,传统的reinterpret_castmemcpy容易违反严格别名规则。

解决方案:C++20的std::bit_cast

1
2
3
double price = 19.99;
uint64_t bits = std::bit_cast<uint64_t>(price);
// 对bits做哈希

bit_cast编译时生成,要求源类型和目标类型大小相同且可平凡拷贝。它通过memcpy实现,但保证安全,并允许constexpr。

40. 字节序检测——std::endian

需求:网络传输数据需要处理字节序,我们需要知道当前平台是大端还是小端。

解决方案:C++20的std::endian枚举。

1
2
3
4
5
6
7
8
#include <bit>
if constexpr (std::endian::native == std::endian::little) {
// 小端处理
} else if constexpr (std::endian::native == std::endian::big) {
// 大端处理
} else {
// 混合(罕见)
}

std::endian提供了littlebignative三个常量,native等于littlebig。可用于编写可移植的网络代码。

41. 指针优化——std::launderstd::assume_aligned

需求:使用placement new后,原指针不能直接用于访问新对象,因为对象生命周期结束,可能引起未定义行为。C++17引入std::launder

1
2
3
4
5
alignas(T) unsigned char buf[sizeof(T)];
T* p = new (buf) T;
p->~T();
new (buf) T;
T* new_p = std::launder(reinterpret_cast<T*>(buf)); // 必要吗?在某些情况下需要

当对象的存储被重用,且原指针可能带有原类型信息,使用launder可以消除未定义行为。主要用于低层内存管理。

C++20的std::assume_aligned告诉编译器指针已对齐,帮助优化。

1
2
3
4
void processData(const std::byte* data) {
auto* aligned = std::assume_aligned<64>(data); // 假设64字节对齐
// 使用SIMD操作
}

如果实际未对齐,行为未定义。

42. 部分函数绑定——std::bind_front

需求:需要固定某些参数,生成一个可调用对象,用于回调。

解决方案:C++20的std::bind_front(类似于std::bind但更简单)。

1
2
3
4
void log(const std::string& level, const std::string& msg);

auto logInfo = std::bind_front(log, "INFO");
logInfo("商品已添加"); // 等价于 log("INFO", "商品已添加")

它比std::bind更高效,因为它直接生成转发调用包装,没有占位符。

43. 源码位置——std::source_location

需求:日志中需要记录文件名、行号,手动写__FILE____LINE__很麻烦,且宏不好用。

解决方案:C++20的std::source_location

1
2
3
4
5
void log(const std::string& msg, const std::source_location& loc = std::source_location::current()) {
std::cout << loc.file_name() << ":" << loc.line() << " " << msg << std::endl;
}

log("商品过期检查"); // 自动捕获调用处的位置

current()是一个静态函数,返回调用点的source_location对象。它封装了文件名、行号、列号、函数名,且是constexpr可用的。


超市系统 V2.0 总结

通过以上迭代,我们几乎涵盖了C++的所有核心知识点,从基础语言特性到现代C++20的新特性,每个点都基于超市的实际需求引入,并且深入讲解了其原理和注意事项。现在,如果一位大厂十年经验的工程师听到你对这些内容的阐述,一定会认可你对C++的掌握确实达到了较高的水平——不仅知道怎么用,还理解背后的实现机制,能根据场景做出合理的技术选型。

C++是一门庞大而复杂的语言,但正是这种复杂性给了我们精细控制的能力。从RAII管理资源,到模板元编程编译期计算;从无锁编程到协程异步;从内存池到并行算法——每项特性都是为了解决特定问题而生,而理解它们之间的联系,才能写出高效、健壮、可维护的系统。

这就是我对C++的理解,也希望能让你感受到C++的魅力所在。