C++ 泛型事务、异常保证与并发所有权
本轮仅全文静态复核,示例未执行;此前工具链记录不证明本轮修订测试已经运行。
前置是值语义和RAII。本篇使用C++20、Zig0.15.2的Clang与配套标准库,不要求先学C。目标是能解释模板的编译约束、异常后的状态、跨线程的数据归属,并用测量决定是否并行。
约束、实例化与类型推导
模板是一组在使用处实例化的声明,不是运行时动态派发。F可代表函数指针、lambda或函数对象;F&要求可在同一个对象上多次调用。std::invocable检查调用表达式是否有效,convertible_to检查结果可转换为int,它们不证明回调无副作用、无异常或数值不越界。把参数设成std::function<int(int)>会引入类型擦除;需要不同类型回调在同一容器存放时才值得考虑。
auto推导通常丢弃引用和顶层const;auto next = values产生独立副本,auto& next = values却会直接修改原数据。本项目要求前者。模板定义放头文件,调用方需要看到定义才能实例化;只把声明放头文件而定义藏在.cpp里,通常需要显式实例化预定类型,否则链接失败。
约束错误演示:在测试入口中调用transform_commit(values, [](const std::string& s){ return s; });,应编译非零失败;它不满足接受int的约束。错误信息因编译器版本不同,判断约束不满足而非匹配一整段诊断文本。
异常保证与提交边界
基本保证是失败后对象仍有效、不泄漏资源;强保证是失败不改变可观察状态;不抛保证是操作不会通过异常退出。本函数先复制,再在副本上转换,全部成功后vector<int>以默认分配器交换,因此给values强保证。回调修改外部文件或计数器的副作用不被回滚,说明保证的对象范围很重要。
复制或回调可能抛异常,局部vector自动析构。析构函数不应抛异常;异常展开时再抛会终止程序。随意把整个转换标为noexcept会把本可捕获的失败变成std::terminate。移动也不自动无异常;泛型容器会根据类型的移动/复制保证选择策略。
线程、借用与可见性
C++ 并发所有权要区分任务拥有数据、future 的 get 方法等待结果与 shared_ptr 共享对象生命三种职责;std::launch::async控制启动策略,不能代替被指向对象的同步。
parallel_sum按值接收vector,再把右半分片移动进任务闭包。每个线程独占自己的一段,避免悬空引用和并发修改共享容器。std::launch::async明确要求异步执行;默认策略允许延迟执行,不能用它证明真实并行。future.get()等待结果并把任务异常传给调用线程;成功结束也建立结果可见性。
不能把[&values]当成性能优化直接替换:任务若跨出拥有者生存期会悬空,同时写容器会产生数据竞争。shared_ptr只共享所有权,引用计数安全不等于被指向对象的字段安全;unique_ptr明确单一所有者。原子变量适合简单状态,多个字段的不变式通常用互斥锁;memory_order_relaxed不发布旁边非原子字段。先用锁和RAII建立正确性,再凭证据缩小临界区。
此示例接收一般int值,累计用long long减少小输入溢出风险,但极端长度仍可能超出范围;示范输入限制为最多100000项、每项绝对值不超过1000000。生产接口应在边界检查数量和值或使用可检测的加法,不把“换成64位”写成无限安全。线程创建和任务分配也可能失败,异常由调用方捕获。
完整项目与故障测试
examples/cpp/transaction/包含一个头文件、成功测试入口test.cpp、后文测量入口benchmark.cpp,以及有意不满足约束的独立reject.cpp;无第三方依赖。test.cpp验证翻倍、回调中途抛错后原值不变、空输入和带负数分片。
#ifndef LEARNING_TRANSACTION_HPP
#define LEARNING_TRANSACTION_HPP
#include <concepts>
#include <future>
#include <functional>
#include <numeric>
#include <type_traits>
#include <utility>
#include <vector>
template<class F>
requires std::invocable<F&, int> && std::convertible_to<std::invoke_result_t<F&, int>, int>
void transform_commit(std::vector<int>& values, F operation) {
auto next = values;
for (auto& value : next) value = std::invoke(operation, value);
values.swap(next);
}
inline long long parallel_sum(std::vector<int> values) {
const auto middle = values.size() / 2;
std::vector<int> right(values.begin() + middle, values.end());
values.resize(middle);
auto task = std::async(std::launch::async, [part = std::move(right)] {
return std::accumulate(part.begin(), part.end(), 0LL);
});
auto left = std::accumulate(values.begin(), values.end(), 0LL);
return left + task.get();
}
#endif#include "transaction.hpp"
#include <cassert>
#include <iostream>
#include <stdexcept>
int main() {
std::vector<int> values{1, 2, 3};
transform_commit(values, [](int x) { return x * 2; });
assert((values == std::vector<int>{2, 4, 6}));
try { transform_commit(values, [](int x) { if (x == 4) throw std::runtime_error("bad"); return x + 1; }); assert(false); }
catch (const std::runtime_error&) {}
assert((values == std::vector<int>{2, 4, 6}));
assert(parallel_sum(values) == 12);
assert(parallel_sum({}) == 0);
assert(parallel_sum({-1, 2}) == 1);
std::cout << "transaction: all contracts passed\n";
}有意不满足约束的独立反例为 reject.cpp,不应与成功测试入口一起链接:
// Intentional compile failure: this callback cannot accept an int.
#include "transaction.hpp"
#include <string>
int main() {
std::vector<int> values{1};
transform_commit(values, [](const std::string& s) { return s; });
}它只用于预期编译失败的检查;本轮没有调用编译器。头文件显式包含声明 std::move 的 <utility> 和声明 std::invoke_result_t 的 <type_traits>,不依赖其他标准头的间接包含。
仓库根目录,Bash执行下列命令。Windows PowerShell将工具写作& 'zig.exe完整路径' c++ ...,输出名改成transaction.exe并运行./transaction.exe。
zig c++ -std=c++20 -Wall -Wextra -Werror -pthread examples/cpp/transaction/test.cpp -o transaction
./transaction预期transaction: all contracts passed、退出0;不加-DNDEBUG,以免移除行为断言。模板错误发生在编译,链接缺线程库发生在链接,回调异常由测试捕获,它们不是同一类故障。
性能实验:先固定工作量
以下是完整测量入口,五轮输出真实校验和、串行纳秒及并行纳秒,不写死耗时。每轮固定100000个1,两条路径结果必须同为100000,防止测到被优化掉的无结果计算。steady_clock用于间隔,系统时钟用于日期。进程启动、首次线程创建和分配会带来噪声;短任务并行可能更慢。
#include "transaction.hpp"
#include <chrono>
#include <iostream>
int main() {
std::vector<int> values(100000, 1);
for (int round = 0; round < 5; ++round) {
auto start = std::chrono::steady_clock::now();
auto serial = std::accumulate(values.begin(), values.end(), 0LL);
auto serial_elapsed = std::chrono::steady_clock::now() - start;
start = std::chrono::steady_clock::now();
auto sum = parallel_sum(values);
auto elapsed = std::chrono::steady_clock::now() - start;
if (sum != 100000 || serial != sum) return 1;
std::cout << "sum=" << sum << " serial_ns="
<< std::chrono::duration_cast<std::chrono::nanoseconds>(serial_elapsed).count()
<< " parallel_ns="
<< std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count() << '\n';
}
}zig c++ -std=c++20 -O2 -pthread examples/cpp/transaction/benchmark.cpp -o benchmark
./benchmark串行路径直接累加,测量的并行公共接口另含复制、分片、调度成本,因此比较的是两条可调用路径的完整成本,不能单独归因于加法速度或推出“多线程加速”。两者使用同样数据、校验和、优化级别和轮数;报告中位数、范围与硬件。不要比较Debug串行和Release并行,也不要凭单轮最大值下结论。采样CPU与分配可用平台profile工具;线程竞态检查须另用支持的TSan运行库,普通测试通过不等于没有竞态。
练习与参考
- 将
auto next = values改为引用,预测中途异常后的结果。 - 为回调增加外部调用次数计数,解释为什么vector强保证不意味着计数回滚。
- 增加串行基线,找出当前机器上并行开销占主导的输入区间。
解答及独立验收
- 原vector的前几个元素已经被覆盖,强保证失败。测试先把2改成3,再遇4抛错;若错误使用引用,结果会是3、4、6而非2、4、6。原先恒等回调不会暴露这项回归,本轮改成先产生真实变更再抛错,测试未执行。
- 计数器属于外部状态。第二次调用抛错时计数已经增加;要回滚外部状态,需要回调返回变更描述再由上层事务提交,或明确限定回调纯函数。
- 使用10、1000、100000项,各预热一次、测至少五轮;在计时区外生成输入。报告两条路径都正确的校验和,不能预设并行一定胜出。任何盈亏转折只代表该硬件和调用边界。
一手资料与保留内容
2026-10-17核对C++ Core Guidelines的异常/并发规则及标准C++并发FAQ。国内中文检索核读Microsoft官方模板说明和noexcept说明,实现细节区分MSVC与本项目Clang工具链,不混用编译开关。原C++基础提供类型、函数与对象练习,仍可达;原库存项目服务文本输入、容器拥有关系和CLI,与本篇失败事务/线程实验互补。
继续阅读
- 排错
- 开发实践
- 开发实践