Releases: wanghenshui/cppweeklynews
C++ 中文周刊 2026-01-09 第193期
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
懒狗忘了
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
No Graphics API
现代GPU已经够成熟了,不需要那些复杂的图形API了。DirectX 12/Vulkan/Metal这些API是13年前为异构硬件设计的,现在GPU都统一了,还搞那么复杂干嘛?
最大的问题是PSO(管线状态对象)排列爆炸,搞出100GB+的缓存
他的想法是简化成类似CUDA的风格:
// 直接用指针分配内存
uint32* numbers = gpuMalloc(1024 * sizeof(uint32));
for (int i = 0; i < 1024; i++) numbers[i] = random();
gpuFree(numbers);
// 根参数直接传指针
struct alignas(16) Data {
float16x4 color;
const uint8* lut;
const uint32* input;
uint32* output;
};
void main(uint32x3 threadId : SV_ThreadID, const Data* data) {
uint32 value = data->input[threadId.x];
data->output[threadId.x] = value;
}
// 图形管线设置简化成这样
GpuRasterDesc rasterDesc = {
.depthFormat = FORMAT_D32_FLOAT,
.colorTargets = {{.format = FORMAT_RG11B10_FLOAT}},
};
GpuPipeline pipeline = gpuCreateGraphicsPipeline(vertexIR, pixelIR, rasterDesc);整个原型API只要150行代码,Vulkan可是20,000+行啊。能简化是好事,不过这得看硬件厂商买不买账了
Deducing the consequences of Windows clipboard text formats on UTF-8
Windows剪贴板对UTF-8的支持就是个坑。问题在于:
- Windows压根没有UTF-8的键盘布局或区域设置
- 转换UTF-16和8位编码时依赖CF_LOCALE,但默认值来自键盘布局语言,跟UTF-8没关系
- 现在per-process的代码页设置更乱了,不同进程对"ANSI"的理解都不一样
结论很简单:UTF-8程序就直接用CF_UNICODETEXT,别折腾其他格式了
不懂windows,就不多嘴了
Modern C++ Firmware 系列
这是个嵌入式固件开发系列,讲怎么在小型关键系统上用现代C++。Part 2讲为什么选C++20,Part 3讲具体的硬性规则
为什么C++20?
利用c++20 的新api来约束
嵌入式的约束很明确:
- WCET(最坏情况执行时间)要可预测
- 内存预算是死的,Flash和SRAM有多少是多少
- 固件要维护好多年
- 嵌入式编译器更新慢,用太新的标准编译器不支持
C++20有这些:
- Concepts做编译期检查
- std::span传缓冲区
- std::array管理固定容量
- std::chrono搞时间
- [[nodiscard]]强制检查返回值
硬性规则
1. 禁用异常和动态分配
热路径里绝对不能分配内存,要用固定容量容器:
class EventQueue final {
public:
static constexpr std::size_t kMaxEventsPerTick = 16U;
[[nodiscard]] bool try_push(std::uint16_t event) noexcept {
if(this->count_ >= kMaxEventsPerTick) {
++this->overflow_count_; // 溢出了就计数,别崩
return false;
}
this->events_[this->count_] = event;
++this->count_;
return true;
}
void clear() noexcept { this->count_ = 0U; }
[[nodiscard]] std::size_t size() const noexcept { return this->count_; }
private:
std::array<std::uint16_t, kMaxEventsPerTick> events_{};
std::size_t count_{0U};
std::uint32_t overflow_count_{0U}; // 记录溢出次数,调试用
};2. 热路径禁用虚函数
用Concepts做编译期多态:
template <typename T>
concept HardwarePlatform = requires(T p) {
{ p.initialize() } noexcept -> std::same_as<bool>;
{ p.read_inputs() } noexcept;
{ p.write_outputs() } noexcept;
};
template <HardwarePlatform P>
void run_tick(P& platform) noexcept {
platform.read_inputs();
platform.write_outputs();
}编译期就能确定调用哪个函数,不用虚函数表那一套
3. 用std::span传缓冲区
[[nodiscard]] bool build_status_line(std::span<char> out) noexcept {
if(out.size() < 8U) { return false; }
out[0] = 'O';
out[1] = 'K';
return true;
}Understanding and mitigating a stack overflow in our task sequencer
Raymond Chen发现他们的task_sequencer会栈溢出。问题在于一堆任务同步完成的时候,协程恢复会递归调用,栈越堆越深
解决办法是强制切换线程,打断递归:
struct task_sequencer
{
task_sequencer(winrt::DispatcherQueue const& queue = nullptr)
: m_queue(queue) {}
template<typename Maker>
auto QueueTaskAsync(Maker&& maker) ->decltype(maker())
{
auto task = [&]() -> Async
{
completer completer{ current };
auto local_maker = std::forward<Maker>(maker);
auto local_queue = m_queue;
co_await suspend;
if (m_queue == nullptr) {
co_await winrt::resume_background(); // 切到后台线程
} else {
co_await winrt::resume_foreground(local_queue); // 或者切到指定队列
}
co_return co_await local_maker();
}();
// ...
}
};线程切换会强制展开栈,就不会溢出了。
Parsing IP addresses quickly (portably, without SIMD magic)
Daniel Lemire测试了几种解析IPv4地址的方法,不用SIMD。结论是手动展开循环最快:
性能对比(Apple M4):
- 手动展开:114指令,3.3纳秒
- 手动循环:185指令,6.2纳秒
- std::from_chars:381指令,14纳秒
std::from_chars慢了4倍多,我操?
手动展开的代码长这样:
std::expected<uint32_t, parse_error> parse_manual_unrolled(const char *p, const char *pend) {
uint32_t ip = 0;
int octets = 0;
while (p < pend && octets < 4) {
uint32_t val = 0;
if (p < pend && *p >= '0' && *p <= '9') {
val = (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
if (val == 0) { // 01.02这种不合法
return std::unexpected(invalid_format);
}
val = val * 10 + (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
val = val * 10 + (*p++ - '0');
if (val > 255) { // 超过255不行
return std::unexpected(invalid_format);
}
}
}
} else {
return std::unexpected(parse_error::invalid_format);
}
ip = (ip << 8) | val;
octets++;
if (octets < 4) {
if (p == pend || *p != '.') {
return std::unexpected(invalid_format);
}
p++;
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}就是把循环展开,少点分支判断
By how much does your memory allocator overallocate?
new char[4096]实际分配多少内存?答案是会多分配一点
Linux:请求4096字节,实际给4104字节,多8字节可以用。开销0.4%,还行
macOS:这个就夸张了。请求3585字节,给你4096字节,浪费14%!macOS喜欢按512字节对齐
可以用 malloc_usable_size查实际能用多少。这种过度分配对小对象影响大,大内存反而无所谓
Software taketh away faster than hardware giveth
Herb Sutter讲为什么C++和Rust还在快速增长
核心观点:软件消耗算力的速度比硬件进步快,性能需求永远满足不了
数据很有意思:
- 2022-2025开发者从3100万涨到4700万,增长50%
- C++和Rust增长最快
- 2025年最大瓶颈是电力供应,不是芯片
关于安全性,他的观点很实在:
- MITRE 2025报告里,10大危险问题只有3个跟语言安全有关
- 79%网络入侵是恶意软件,不是代码漏洞
- C++漏洞率比C低多了
C++26要加的新东西:
- 未初始化变量不再UB
- 标准库加边界检查(强化模式)
- 合约支持
性能需求一直在涨,C++不会过时
Unsigned char std::basic_string<> in C++
用 std::basic_string<uint8_t>处理二进制数据?别这么干了
问题在于 std::basic_string依赖 std::char_traits<T>,标准只保证 char/wchar_t这些类型有。uint8_t以前是"意外"能用,LLVM 19.1.0直接把基础模板删了
解决办法:
- 直接用
std::vector<uint8_t>,不要折腾 - 或者自己特化
std::char_traits<uint8_t>
vector够了
Implementing vector<T>
手写vector,教学向的文章。关键是要分离内存分配和对象构造,还要处理异常安全
template <typename T>
class vector {
private:
T* m_data;
std::size_t m_size; // 实际元素数量
std::size_t m_capacity; // 容量
};reserve要这么写才异常安全:
void reserve(size_type new_capacity){
if(new_capacity <= capacity()) return;
auto ptr = allocate_helper(new_capacity); // 先分配新内存
try {
copy_old_storage_to_new(m_data, m_size, ptr); // 拷贝可能抛异常
} catch(std::exception& ex){
deallocate_helper(ptr); // 失败了释放新内存
throw; // 继续抛,对象状态不变
}
std::destroy(m_data, m_data + m_size); // 成功了才销毁旧数据
deallocate_helper(m_data);
m_data = ptr;
m_capacity = new_capacity;
}先做可能失败的操作,成功了再修改状态。这是异常安全的基本套路
文章写得挺详细,不过说实话,谁手写啊。面试可能会考?
Time in C++: Additional clocks in C++20
C++20加了5种新时钟,因为"世界运行在多个时间尺度上"
- utc_clock:有闰秒的UTC时间
- tai_clock:国际原子时间,1958年开始,不含闰秒
- gps_clock:GPS时间,1980年开始
- file_clock:文件系统时钟,跟
std::filesystem配套 - local_t:本地时间,但不指定时区
local_t要跟时区配合用:
auto local = std::chrono::local_time<std::chrono::minutes>{ ... };
auto tz = std::chrono::locate_zone("Europe/Berlin");
auto sys_time = tz->to_sys(local); // 转成系统时间Raymond Chen的内存块交换系列
Raymond Chen写了一系列文章,讲怎么只用前向迭代器交换内存块。为什么要研究这个?因为 std::rotate只需要前向迭代器,但常见的实现方法需要双向迭代器
- How can you swap two adjacent blocks of memory using only forward iterators?
- How can you swap two non-adjacent blocks of memory using only forward iterators
- Swapping two blocks of memory that reside inside a larger block, in constant memory, refinement
相邻块交换
设三个指针 first、mid、last,要把块A和块B交换位置。思路是逐个交换元素,直到较小的块移动完,然后递归处理剩余部分
template<typename ForwardIt>
void rotate_adjacent(ForwardIt first, ForwardIt mid, ForwardIt last) {
if (first == mid || mid == last) return;
auto p = first;
auto q = mid;
while (true) {
std::iter_swap(p++, q++); // 交换元素
if (p == mid) {
// 块A用完了,块B还有剩余
if (q == last) return; // 都用完了
mid = q; // 递归处理剩余的
} else if (q == last) {
// 块B用完了,块A还有剩余
// 递归处理剩余的
return rot...C++ 中文周刊 2025-12-19 第192期
公众号
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
C++结构化迭代
这篇文章讲解如何用安全的range-based循环逐步替代容易出错的索引循环。
传统for循环的陷阱:
for (auto i = 0; i <= vec.size(); ++i) // 应该是 <,不是 <=
use(vec[i]);基本range-for(C++11):
for (Record const& rec : records)
use(rec);反向迭代(C++20):
using std::views::reverse;
for (Record const& rec : reverse(records))
use(rec);同时迭代两个序列(C++23):
using std::views::zip;
for (auto [name, rec] : zip(names, records))
use(name, rec);带索引的迭代(C++20+):
using std::views::iota;
using std::views::zip;
for (auto [i, rec] : zip(iota(0), records))
use(i, rec);简化的索引迭代(C++23):
using std::views::enumerate;
for (auto [i, rec] : enumerate(records))
use(i, rec);需要传统循环的场景:
// 复杂索引计算
for (int i = 0; i != records.size(); i = records[i])
use(records[i]);能用range就别手写索引。
用Lambda工厂创建Functor
文章讨论了从C++11前基于struct的functor到现代lambda方案的演进,提倡"functor工厂"——返回lambda的函数。
传统Functor Struct(C++11前):
struct key_less {
bool operator()(T const& lhs, T const& rhs) const {
return lhs.key < rhs.key;
}
};
std::sort(c.begin(), c.end(), key_less{});带上下文的Functor:
struct indirect_less {
indirect_less(std::vector<int> const& indirection)
: indirection_(indirection) {}
bool operator()(T const& lhs, T const& rhs) const {
return indirection_[lhs.key] < indirection_[rhs.key];
}
private:
std::vector<int> const& indirection_;
};现代Lambda方式:
auto indirect_less = [&](T const& lhs, T const& rhs) {
return indirection[lhs.key] < indirection[rhs.key];
};推荐的Lambda工厂模式:
auto make_indirect_less(std::vector const& indirection) {
return [&](T const& lhs, T const& rhs) {
return indirection[lhs.key] < indirection[rhs.key];
};
}更快的double转string
Victor Zverovich展示了新的浮点转字符串算法zmij,显著超越现有方法,原则是"不做比做点什么更快"。
性能基准:
- 比Dragonbox快68%(之前带正确性证明的领先者)
- 比Schubfach实现快2倍
- 比
std::to_chars(libc++)快3.5倍 - 比Google的double-conversion (Grisu3)快6.8倍
- 比macOS上的
sprintf快59倍
单个double转换:Apple M1上约10-20纳秒。
关键代码 - 优化的对数近似:
constexpr int log10_2_sig = 315'653;
constexpr int log10_2_exp = 20;
auto floor_log10_pow2(int e) noexcept -> int {
return e * log10_2_sig >> log10_2_exp;
}快速除法和取模:
inline auto divmod100(uint32_t value) noexcept -> divmod_result {
assert(value < 10'000);
constexpr int exp = 19;
constexpr int sig = (1 << exp) / 100 + 1;
uint32_t div = (value * sig) >> exp;
return {div, value - div * 100};
}主要改进:
- 减少候选数:从2-4个候选选择减少到1-3个
- 更少乘法:特别是在短路径
- 无分支操作:消除预测不良的分支
- 查找表:数字对输出减半整数乘法
- 简化不规则舍入:无分支处理边缘情况
应用:
- 集成进{fmt}库
- Thrift的JSON序列化
- 可能通过ISO C++ paper P2587改进
std::to_string
代码仓库:https://github.com/vitaut/zmij
vpternlog实现有符号饱和运算
这篇文章探讨如何用Intel的vpternlog{d,q}指令——三操作数位逻辑运算——在x86-64处理器上实现32位和64位有符号饱和运算。
vpternlog指令:"三个操作数的位三元逻辑,能实现任何三操作数布尔函数",通过8位查找表索引三个输入操作数的位。
**饱和运算:**防止溢出/下溢回绕,把结果钳位到最大值(0x7FFFFFFF)或最小值(0x80000000)。
检测逻辑:
加法,输入符号相同但结果符号不同时饱和:
- LUT常量:
0b01000010(0x42) - 公式:
~(a ^ b) & (a ^ c)
减法,输入符号不同且结果符号与第一操作数不同时饱和:
- LUT常量:
0b00011000(0x18) - 公式:
(a ^ b) & (a ^ c)
32位有符号饱和加法:
__m128i _mm_adds_epi32(__m128i a, __m128i b)
{
__m128i Result = _mm_add_epi32(a, b);
const std::uint32_t Lut = 0b01000010;
const __m128i SaturationCheck = _mm_ternarylogic_epi32(a, b, Result, Lut);
const __mmask8 SaturationMask = _mm_movepi32_mask(SaturationCheck);
Result = _mm_mask_srai_epi32(Result, SaturationMask, Result, 31);
Result = _mm_mask_xor_epi32(
Result, SaturationMask, Result, _mm_set1_epi32(0x80000000u)
);
return Result;
}32位有符号饱和减法:
__m128i _mm_subs_epi32(__m128i a, __m128i b)
{
__m128i Result = _mm_sub_epi32(a, b);
const std::uint32_t Lut = 0b00011000;
const __m128i SaturationCheck = _mm_ternarylogic_epi32(a, b, Result, Lut);
const __mmask8 SaturationMask = _mm_movepi32_mask(SaturationCheck);
Result = _mm_mask_srai_epi32(Result, SaturationMask, Result, 31);
Result = _mm_mask_xor_epi32(
Result, SaturationMask, Result, _mm_set1_epi32(0x80000000u)
);
return Result;
}64位有符号饱和加法:
__m128i _mm_adds_epi64(__m128i a, __m128i b)
{
__m128i Result = _mm_add_epi64(a, b);
const __m128i SaturationCheck = _mm_ternarylogic_epi64(a, b, Result, 0b01000010);
const __mmask8 SaturationMask = _mm_movepi64_mask(SaturationCheck);
Result = _mm_mask_srai_epi64(Result, SaturationMask, Result, 63);
Result = _mm_mask_xor_epi64(
Result, SaturationMask, Result, _mm_set1_epi64x(0x8000000000000000)
);
return Result;
}64位有符号饱和减法:
__m128i _mm_subs_epi64(__m128i a, __m128i b)
{
__m128i Result = _mm_sub_epi64(a, b);
const __m128i SaturationCheck = _mm_ternarylogic_epi64(a, b, Result, 0b00011000);
const __mmask8 SaturationMask = _mm_movepi64_mask(SaturationCheck);
Result = _mm_mask_srai_epi64(Result, SaturationMask, Result, 63);
Result = _mm_mask_xor_epi64(
Result, SaturationMask, Result, _mm_set1_epi64x(0x8000000000000000)
);
return Result;
}算法总结:
- 执行算术运算(加/减)
- 用
vpternlog根据符号位检测溢出/下溢 - 通过
vpmov{d,q}2m将检测结果转换为执行掩码 - 算术右移结果以扩展符号位
- XOR饱和常量生成适当边界
C++矩阵乘法
Marius Bancila探讨C++中高效矩阵乘法的各种实现策略,回应Python声称在此任务上优于C++的病毒式说法。文章对比了1000×1000矩阵的多种实现。
数据设置:
constexpr std::size_t N = 1000;
std::vector<double> A_data(N * N);
std::vector<double> B_data(N * N);
std::vector<double> C_data(N * N);
std::random_device rd{};
std::mt19937 rng(rd());
std::uniform_real_distribution<double> dist(-1000, 1000);
for (auto& v : A_data) v = dist(rng);
for (auto& v : B_data) v = dist(rng);朴素实现(~850ms):
void naive_multiplication(std::vector<double> const & A,
std::vector<double> const & B,
std::vector<double> & C,
std::size_t const N)
{
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = 0; j < N; ++j) {
C[i * N + j] = 0;
for (std::size_t k = 0; k < N; ++k) {
C[i * N + j] += A[i * N + k] * B[k * N + j];
}
}
}
}优化的朴素实现(~700ms):
void naive_optimized_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = 0; j < N; ++j) {
double sum = 0;
for (std::size_t k = 0; k < N; ++k) {
sum += A[i * N + k] * B[k * N + j];
}
C[i * N + j] = sum;
}
}
}关键优化:"用独立变量计算和而不是每次乘法后写入C矩阵",通过寄存器存储提升约15%。
并行化版本(~170ms):
void parallelized_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
std::vector<std::size_t> rows(N, 0);
std::for_each(
std::execution::par,
rows.begin(), rows.end(),
[&](std::size_t i) {
for (std::size_t j = 0; j < N; ++j) {
double sum = 0;
for (std::size_t k = 0; k < N; ++k)
sum += A[i * N + k] * B[k * N + j];
C[i * N + j] = sum;
}
});
}性能:比朴素方法快5倍。
OpenBLAS库(~10ms):
#pragma comment(lib,"libopenblas.lib")
#include "cblas.h"
void openblas_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
blasint const N)
{
cblas_dgemm(
CblasRowMajor,
CblasNoTrans,
CblasNoTrans,
N, N, N,
1.0,
A.data(), N,
B.data(), N,
0.0,
C.data(), N
);
}性能:比朴素方法快80倍;比并行化快15倍。
C++26标准方法(概念):
#include <linalg>
#include <mdspan>
#include <vector>
void linalg_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
std::mdspan A_view(A.data(), N, N);
std::mdspan B_view(B.data(), N, N);
std::mdspan C_view(C.data(), N, N);
std::linalg::matrix_product(A_view, B_view, C_view);
}别自己写矩阵乘法了,用OpenBLAS或等C++26的linalg。
Loop Unswitching
Loop Unswitching咋翻译?
核心概念:
编译器识别出循环内的条件在执行期间永不改变。与其重复评估该条件,不如为每个可能分支创建两个专用循环版本。"编译器意识到bool squared值在整个循环中不变,决定复制循环。"
原始代码示例:
int sum_values(std::vector<int> const& values, bool squared) {
int s...C++ 中文周刊 2025-12-12 第191期
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
std::move doesn't move anything: A deep dive into Value Categories
这篇文章深入讲解了C++的值类别(lvalue、prvalue、xvalue)和std::move的实质。
值类别概念:
C++11之前只有lvalue和rvalue,C++11引入了移动语义后,值类别变成了三种:
- lvalue - 有名字的对象,可以取地址
- prvalue - 纯右值,临时对象
- xvalue - 将亡值,即将被移动的对象
std::move的真相:
std::move实际上只是一个cast,它把lvalue转成xvalue,允许编译器调用移动构造函数。实现非常简单:
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
return static_cast<std::remove_reference_t<T>&&>(t);
}关键示例 - std::move不保证移动:
struct A {
std::string s;
A(std::string str) : s(std::move(str)) {
// str被move后,依然是有效的lvalue
std::cout << str.length() << '\n'; // 打印0,已被移走
}
};常见陷阱:
void process(const std::string& s) {
std::string local = std::move(s); // 没用!const对象不能移动
}
std::string get_string() {
std::string s = "hello";
return std::move(s); // 画蛇添足!阻止了NRVO优化
}std::move只是改变值类别的工具,真正的移动发生在移动构造/赋值函数里。return语句不要用std::move,会妨碍NRVO。
A faster full-range Leap Year function
作者发明了一个新的闰年判断算法,比传统方法快。
传统算法:
bool is_leap_year(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}需要2-3次除法和多次比较。
新算法 - modulus-equality技巧:
bool is_leap_year_fast(int year) {
return (year & 3) == 0 && ((year % 25) != 0 || (year & 15) == 0);
}核心思路:
year % 4 == 0等价于(year & 3) == 0(位运算更快)year % 100 == 0可以分解为(year % 4 == 0) && (year % 25 == 0)year % 400 == 0可以分解为(year % 16 == 0) && (year % 25 == 0)
性能测试结果:
在作者的机器上,新算法比传统算法快约30%。关键在于减少了除法运算,用更便宜的位运算和模25运算替代。
完整优化版本:
bool is_leap_year_optimized(int year) {
// 快速路径:大多数年份能被4整除但不是100的倍数
if ((year & 3) != 0) return false;
unsigned int mod25 = year % 25;
if (mod25 != 0) return true;
// 慢速路径:检查400的倍数
return (year & 15) == 0;
}怎么感觉不是新东西
15 Different Ways to Filter Containers in Modern C++
这篇文章展示了C++中过滤容器的15种方法,从传统循环到C++23的新特性。
问题场景: 从vector中筛选出偶数
方法1 - 原始循环:
std::vector<int> filter_even_raw_loop(const std::vector<int>& input) {
std::vector<int> result;
for (size_t i = 0; i < input.size(); ++i) {
if (input[i] % 2 == 0) {
result.push_back(input[i]);
}
}
return result;
}方法2 - Range-based for:
std::vector<int> filter_even_range_for(const std::vector<int>& input) {
std::vector<int> result;
for (int val : input) {
if (val % 2 == 0) {
result.push_back(val);
}
}
return result;
}方法3 - std::copy_if + back_inserter:
std::vector<int> filter_even_copy_if(const std::vector<int>& input) {
std::vector<int> result;
std::copy_if(input.begin(), input.end(),
std::back_inserter(result),
[](int x) { return x % 2 == 0; });
return result;
}方法4 - 就地删除 erase-remove:
void filter_even_erase_remove(std::vector<int>& vec) {
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int x) { return x % 2 != 0; }),
vec.end());
}方法5-7 - C++20 Ranges:
// Views - 惰性求值
auto filter_even_view(const std::vector<int>& input) {
return input | std::views::filter([](int x) { return x % 2 == 0; });
}
// 转换回vector
std::vector<int> filter_even_ranges(const std::vector<int>& input) {
auto filtered = input | std::views::filter([](int x) { return x % 2 == 0; });
return std::vector<int>(filtered.begin(), filtered.end());
}
// C++23 ranges::to
std::vector<int> filter_even_ranges_to(const std::vector<int>& input) {
return input
| std::views::filter([](int x) { return x % 2 == 0; })
| std::ranges::to<std::vector>();
}方法8 - 并行算法:
std::vector<int> filter_even_parallel(const std::vector<int>& input) {
std::vector<int> result;
std::copy_if(std::execution::par,
input.begin(), input.end(),
std::back_inserter(result),
[](int x) { return x % 2 == 0; });
return result;
}方法9-15: 文章还介绍了自定义迭代器、表达式模板、C++23 generator等更高级的方法。
C++提供了从底层到高层的各种过滤容器的方法,ranges让代码更简洁,但底层方法在性能关键场景仍有用。
Unrolling loops
循环展开是编译器的常见优化,但需要编译时已知循环边界。
基本循环:
void process_array(int* arr, size_t n) {
for (size_t i = 0; i < n; ++i) {
arr[i] = arr[i] * 2 + 1;
}
}如果n是运行时变量,编译器很难展开。
编译时已知边界:
template<size_t N>
void process_array_fixed(int (&arr)[N]) {
for (size_t i = 0; i < N; ++i) {
arr[i] = arr[i] * 2 + 1;
}
}这种情况下,编译器能把循环完全展开:
// N=4时,编译器生成:
arr[0] = arr[0] * 2 + 1;
arr[1] = arr[1] * 2 + 1;
arr[2] = arr[2] * 2 + 1;
arr[3] = arr[3] * 2 + 1;部分展开:
对于较大的循环,编译器会做部分展开:
// 原循环
for (size_t i = 0; i < 1000; ++i) {
process(i);
}
// 编译器展开成4路并行
for (size_t i = 0; i < 1000; i += 4) {
process(i);
process(i+1);
process(i+2);
process(i+3);
}好处:
- 减少分支预测失败
- 提高指令级并行度
- 更好的寄存器利用
代价:
- 代码体积增大
- 指令缓存压力
现代编译器已经很智能了,手动展开通常没必要,但理解原理有助于写出编译器友好的代码。
A Guest Editorial
这篇编者按介绍了C++20协程特性,协程是"可以在中间暂停的函数",之后可以恢复执行并保持状态。
三个关键字:
co_yield- 输出值然后挂起co_await- 等待输入然后挂起co_return- 终止执行
传统阻塞式服务器:
void session(Socket sock){
char buffer[1024];
int len = sock.read({buffer});
sock.write({buffer,len});
log(buffer);
}阻塞在read和write上,无法处理其他连接。
协程版本:
Task<void> session(Socket sock){
char buffer[1024];
int len = co_await sock.async_read({buffer});
co_await sock.async_write({buffer,len});
log(buffer);
}用co_await标记暂停点,代码看起来是顺序的,但执行是异步的。不用回调地狱,不用手写状态机。
实际应用:
- 异步计算 - 简化服务器代码,顺序代码加暂停点替代回调嵌套
- 惰性求值 - 延迟计算,支持无限列表编程
架构组件:
四个核心要素协同工作:ReturnType(定义接口)、Promise(生产者侧数据存储)、coroutine_handle(帧指针)、Awaitable(挂起机制)。
协程初看复杂,但比回调简洁太多,暂停点清晰可控。
Why I Don't Use AI
作者Andy Balaam列举了不使用大语言模型的四个主要理由:
1. 环境影响
数据中心能耗巨大,预计到2030年翻倍。爱尔兰的数据中心消耗了近五分之一的电力供应。AI公司理念就是无限制消耗资源,不可持续。
2. 工人剥削
数百万工人以极低工资(每小时1.32-2美元)标注数据训练模型,很多人处理恶心内容后心理创伤,"渗透进你大脑甩不掉"。
3. 输出不可靠且危险
AI系统会自信地输出错误信息,更可怕的是,聊天机器人据说鼓励过青少年自杀,引导用户进入"妄想螺旋",心理健康风险严重。
4. 版权侵犯
AI训练违反授权协议,复制版权材料不署名也不遵守许可证,伤害开源创作者和依赖引用流量的商业网站。
另外,生产力提升缺乏实证支持,AI可能只是裁员借口。
作者态度很明确,AI公司的商业模式建立在剥削和侵权基础上,不值得支持。
Concurrency Flavours
Lucian Radu Teodorescu探讨了为什么需要并发,以及不同的动机如何塑造不同的并发方法。
并发定义:
并发在数学上是"工作项的严格偏序",任务可以在时间上重叠而不是顺序执行。
四种主要动机:
1. 响应性(Responsiveness)
保持系统交互性,确保关键组件(如UI线程)始终可用。餐厅类比:主持人必须在2分钟内自由接待顾客。
2. 延迟优化(Latency Optimization)
通过做更多总工作来更快完成单个任务。并行前缀和算法展示了这种权衡——总操作更多但完成更快。
3. 吞吐量最大化(Throughput Maximization)
单位时间处理更多工作。多个厨师同时处理不同菜品增加客户容量,即使单个菜品时间增加。
4. 分解(Decomposition)
将系统结构化为独立可管理的组件,为了清晰而非性能。餐厅的专门角色或Web服务器的独立请求处理器。
关键区分:
- 并发 vs 并行:并发是结构性的(任务可能重叠);并行是实际同时执行
- 异步:关注控制流和避免阻塞,通常通过协程
- 多线程:使用OS线程和共享内存的特定实现
核心信息:理解为什么需要并发比如何实现更重要。动机决定方案选择。
Coroutines – A Deep Dive
Quasar Chunawala深入探讨了C++协程的底层机制。
Promise类型:
协程的返回类型必须定义promise_type结构体,这类似异步编程中的future/promise,作为"调用者与协程交互的接口"。
示例1 - 基本Promise类型实现:
#include <coroutine>
#include <print>
struct Task{
struct promise_type{
Task get_return_object(){
std::println("get_return_object()");
return Task{ *this };
}
void return_void() noexcept {
std::println("return_void()");
}
void unhandled_exception() noexcept {
std::println("unhandled_exception()");
}
std::suspend_always initial_suspend() noexcept {
std::println("initial_suspend()");
return {};
}
std::suspend_always final_suspend() noexcept{
std::println("final_suspend()");
return {};
}
};
explicit Task(promise_type&){
std::println("Task(promise_type&)");
}
~Task() noexcept{
std::println("~Task()");
}
};
Task coro_func(){
co_return;
}
int main(){
coro_func();
}关键函数说明:
get_return_object()- 构造并返回给调用者的对象return_void()或return_value()- 处理co_return语句initial_suspend()和final_suspend()- 控制启动和完成时的执行unhandled_exception()- 处理抛出的异常
示例2 - 产出协程(使用co_yield):
#include <coroutine>
#include <print>
#include <iostream>
#include <string>
struct Task{
struct promise_type{
std::string output_data{};
Task get_return_object(){
return Task{ *this };
}
void return_void() noexcept {}
void unhandled_exception() noexcept {}
std::suspend_always initial_suspend() noexcept{ return {}; }
std::suspend_always final_suspend() noexcept{ return {}; }
std::suspend_always yield_value(std::string msg) noexcept{
output_data = std::move(msg);
return {};
}
};
std::coroutine_handle<promise_type> handle{};
explicit Task(promise_type& promise)
: handle { std::coroutine_handle<promise_type>::from_promise(promise) }
{}
~Task() noexcept{
if(handle)
handle.destroy();
}
std::string get(){
if(!handle...C++ 中文周刊 2025-12-05 第190期
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
本期文章由 孙咖啡 赞助 在此表示感谢
悲报,我尝试了一下claude整理周刊,发现比我写的快很多。本周刊正式自动化
我又被替代了,以后可能有机会周更了
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
许传奇写的模块文章。
YexuanXiao 投稿
另外补充一下,不需要C++23就可以使用标准库模块,文章里说的“项目已更新到最新的编译器与语言标准(至少 -std=c++23)”的原因是C++23才提供std模块(体验更好),但实际上三个活跃的标准库实现都支持在C++20模式使用std模块,这点我在C++Reference标准库页的注解一节特意写上了“libstdc++, libc++, and STL all support using standard library modules in C++20 mode.”。
Structured iteration
文章倡导在C++中使用更安全的迭代方式,从低级构造(goto、传统for循环)逐步转向现代基于范围的替代方案,使错误更难引入且更容易发现。
传统for循环的问题在于"过于灵活",开发者容易引入off-by-one错误、不正确的条件判断或意外修改循环变量。编译器无法防止这些问题,因为这种灵活性有时是有意为之的。
常见的错误示例:
// 错误1:off-by-one错误
for (auto i = 0; i <= vec.size(); ++i) // 应该是 <,不是 <=
use(vec[i]);
// 错误2:嵌套循环中修改错误的变量
for (auto i = 0; i != widths.size(); ++i)
for (auto j = 0; j != heights.size(); ++i) // 应该是 ++j
use(widths[i], heights[j]);
// 错误3:无符号数反向迭代的陷阱
for (auto i = vec.size() - 1; i >= 0; --i) // i永远 >= 0,死循环!
use(vec[i]);现代安全替代方案:
// 1. 基于范围的循环(最安全的简单情况)
for (Record const& rec : records)
use(rec);
// 2. 反向迭代(C++20)
using std::views::reverse;
for (Record const& rec : reverse(records))
use(rec);
// 3. 多个序列同时迭代(C++23)
using std::views::zip;
for (auto [name, rec] : zip(names, records))
use(name, rec);
// 4. 带索引的迭代
using std::views::iota;
using std::views::zip;
for (auto [i, rec] : zip(iota(0), records))
use(i, rec);
// 5. C++23简写 - enumerate
using std::views::enumerate;
for (auto [i, rec] : enumerate(records))
use(i, rec);笔者:现代C++的范围库(Ranges)真是个好东西。以前写反向循环要小心翼翼处理索引,现在直接reverse(records)搞定。C++23的enumerate更是Python程序员的福音。
C++20's std::source_location in action
Andreas Fertig解释了std::source_location如何在C++20中现代化获取源代码信息的方式,消除了对基于宏的方法(如__FUNCTION__和__LINE__)的需求。
传统宏方法:
void Assert(bool condition, std::string_view msg,
std::string_view function, int line) {
if(not condition) {
std::clog << function << ':' << line
<< ": " << msg << '\n';
}
}
#define ASSERT(condition, msg) \
Assert(condition, msg, __FUNCTION__, __LINE__)C++20重构版本:
void Assert(bool condition, std::string_view msg,
std::source_location location =
std::source_location::current()) {
if(not condition) {
std::clog << location.function_name() << ':'
<< location.line() << ": " << msg << '\n';
}
}
// 使用时不需要宏:
Assert(1 != 2, "Not met");关键特性:"静态成员函数current从调用侧获取信息",这使其在C++的默认参数中独树一帜。
You can't fool the optimiser
文章展示了编译器能够识别数学上等价的代码模式并对它们进行相同的优化,无论源代码看起来多么混淆。
编译器将代码转换为中间表示(IR)——"一种简化的抽象形式,更易于分析"。相同操作的不同实现会被转换为规范形式,使它们在代码生成期间无法区分。
实际例子:多种计算加法的方式(循环、递归、复杂逻辑)都会编译为单个ARM指令:add w0, w1, w0。即使是尾递归版本也能受益于优化。
这意味着程序员可以优先考虑代码清晰性而不牺牲性能;编译器会自动处理高效转换。
Multiplying with a constant
Matt Godbolt探讨了现代x86编译器如何优化常量乘法,展示了智能指令选择通常超越手动优化尝试。
lea(load effective address)指令在乘法任务中非常通用,允许通过寻址模式高效地乘以1、2、4或8。
编译器优化示例:
- 乘以2:将
rdi与自身相加 - 乘以3:计算为
rdi + rdi*2 - 乘以25:计算为
(x × 5) × 5,使用重复加法-乘法 - 乘以522:编译器选择实际的
imul指令而不是手动的移位-加法分解
关键发现:当Godbolt手动优化522为(x << 9) + (x << 3) + (x << 1)时,编译器仍然"发现了我们在做什么"并恢复为乘法,识别出在现代CPU上这更快。
"编译器知道所有用于乘法的移位和加法技巧,以及它们何时合适。"
Slicing makes you cry
"C++中的切片是当你有一个多态类型并将基类值分配给子类的值时发生的情况。"这种非预期行为只复制派生对象的基类部分,丢失子类特定数据。
问题演示:
struct A {
int x_;
A(int x) : x_(x) {}
};
struct B : A {
int y_;
B(int x, int y) : A(x), y_(y) {}
};
int main() {
B b{5, 3};
A a = b; // 切片发生!y_ 丢失了
}防止策略1:显式删除(C++17)
struct A {
int x_;
A(int x) : x_(x) {}
template <typename T, typename = std::enable_if_t<
std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A(const T&) = delete;
template <typename T, typename = std::enable_if_t<
std::is_base_of_v<A, T> && !std::is_same_v<A, T>>>
A& operator=(const T&) = delete;
// 同样需要删除移动操作
};防止策略2:使用CRTP的通用模板(C++20)
template <typename T>
struct DontSlice {
DontSlice() = default;
DontSlice(const derived_from<T> auto&) = delete;
DontSlice& operator=(const derived_from<T> auto&) = delete;
DontSlice(derived_from<T> auto&&) = delete;
DontSlice& operator=(derived_from<T> auto&&) = delete;
};
struct A : DontSlice<A> {
int x_;
A(int x) : x_(x) {}
using DontSlice<A>::DontSlice;
};笔者:对象切片是C++中最阴险的坑之一。你以为复制了整个对象,实际上只复制了一半。现代C++用concepts可以优雅地防止这个问题,但错误信息仍然可能让人头疼。建议配合Clang-Tidy的cppcoreguidelines-slicing检查使用。
When should a =delete'd constructor be explicit?
文章探讨了在C++中何时应该给被删除(=delete)的构造函数标记为explicit。
核心观点: "当你=delete一个构造函数时,通常是为了'推翻'另一个更贪婪的构造函数。让你删除的构造函数的explicit特性与你打算推翻的构造函数相匹配。"
问题场景:
template<class T>
struct AtomicRef {
explicit AtomicRef(T&);
MAYBE_EXPLICIT AtomicRef(T&&) = delete;
};
struct X {
X(int);
};
void f(AtomicRef<const int>);
void f(X);
void test() {
f(42);
}关键差异:当MAYBE_EXPLICIT为空时,调用f(42)会产生歧义。但标记为explicit时,调用变得明确且有效。
C++26允许atomic_ref<const T>,但需要删除T&&构造函数以防止临时引用问题。这导致了标准库首次使用删除的explicit构造函数。
Exploring C++20 std::chrono - Calendar Types
C++20标准化了日历类型,实现类型安全的日期操作,无需脆弱的整数算术。库引入了days、weeks、years和months的持续时间类型,以及14+种日历类型如year、month、day、weekday和year_month_day。
通过/操作符创建:
auto ymdOct = std::chrono::year{1996} / 10 / 31; // 1996年10月31日
auto ym = std::chrono::year{2025} / 11; // 2025年11月
auto mw = 11 / std::chrono::Monday[3]; // 11月的第3个星期一算术操作示例:
// day操作
std::chrono::day oneDay{7};
oneDay += std::chrono::days(20); // 结果为第27天
// month操作
std::chrono::month oneMonth{std::chrono::September};
oneMonth += std::chrono::months(20); // 结果为5月
// year_month_day操作
std::chrono::year_month_day today{std::chrono::year{2025}/11/22};
auto future = today + std::chrono::years{10}; // 2035-11-22
auto result = future - std::chrono::months{120}; // 2025-11-22
// weekday操作(使用模运算)
std::chrono::weekday aday = std::chrono::Sunday;
aday += std::chrono::days{22};
std::chrono::days diff = std::chrono::Monday - std::chrono::Tuesday; // 6天日精度算术:
namespace chr = std::chrono;
const auto today = chr::sys_days{chr::floor<chr::days>(chr::system_clock::now())};
auto importantDate = chr::year{2011} / chr::July / 21;
const auto delta = (today - chr::sys_days{importantDate}).count();
std::print("{} was {} days ago!\n", importantDate, delta);Maybe somebody can explain to me how weak references solve the ODR problem
Raymond Chen质疑Reddit声称弱函数可以解决在条件编译影响数据布局时的ODR(One Definition Rule)问题。
当Widget结构基于EXTRA_WIDGET_DEBUGGING条件性地包含Logger成员时,直接成员访问在调用点被内联,根据编译标志设置使用不同的偏移量。当调用者和被调用者对标志设置不一致时,这会导致崩溃。
关键论点: "如果你正在改变数据布局(如我们在添加Logger成员的情况下),你可以弱化任何访问数据成员的函数,但这对访问这些数据成员的类使用者没有帮助,因为数据成员访问被内联到调用点。"
建议的解决方案:
template<bool debugging>
struct WidgetT { /* ... */ };
#ifdef EXTRA_WIDGET_DEBUGGING
using Widget = WidgetT<true>;
#else
using Widget = WidgetT<false>;
#endif这允许混合编译模式——某些代码使用WidgetT<true>,其他使用WidgetT<false>。
Why does XAML break down when I have an element that is half a billion pixels tall?
Raymond Chen解释了为什么当元素达到极端尺寸(例如5亿像素高)时XAML会失败。在96 DPI下,这相当于超过82英里——远超任何实际显示能力。
技术根本原因: "XAML内部使用单精度IEEE浮点值"以提高性能。单精度只能精确表示约±1670万以内的整数——远小于5亿像素。
实际后果:
- 超出可表示范围的值失去精度
- 分数像素计算失败,导致级联舍入错误
- 渲染变得不可能
解决策略:
- 将虚拟元素缩小到约3倍用户屏幕大小
- 通过在用户滚动时动态调整元素来虚拟化内容
- 在任何给定时刻仅实现可见部分
这种方法类似于ListView等虚拟化控件高效管理大型数据集的方式。
Event-driven flows
Andrzej Krzemieński认为,协程在处理引用参数和对象生命周期时,与其他异步函数实现在生命周期管理风险方面并无本质不同。两者在处理引用参数和对象生命周期时都有类似的陷阱。
生命周期挑战: 与基于栈的自动变量管理资源的同步函数不同,异步函数必须跨多个回调调用保留状态。"基于栈的自动变量不能用于管理会话状态,因为它跨越不同的作用域。"
代码模式对比:
传统回调方法(Boost.ASIO):
void session(Socket sock) {
auto s = make_shared<State>(move(sock));
exec.async_read(s->sock, s->buffer, [s](error_code ec, int len) {
if (!ec)
exec.async_write(s->sock, {s->buffer, len}, [s](error_code ec, int) {
if (!ec) finish(s);
});
});
}协程版本:
task<void> session(Socket sock) {
Buffer buffer;
int len = co_await exec.co_read(sock, buffer);
co_await exec.co_write(sock, {buffer, len});
finish();
}面向对象与共享所有权:
class Session : public enable_shared_from_this<Session> {
void operator()() {
exec.async_read(_s->sock, _s->buffer,
[shared_from_this()](error_code ec, int len) {
on_read(ec, len);
});
}
};协程提供了语法优雅性并减少了样板代码,但开发者必须理解异步语义(而非实现机制)才是产生风险的根源。
Binary Trees: using unique_ptr
文章研究了使用C++的std::unique_ptr实现二叉树与使用原始指针手动内存管理以及基于向量索引方法的性能影响。
关键发现:
- 基于向量的索引总体上仍然最快
unique_ptr相比向量增加了1.7-2.2倍的开销(O2/O3优化级别)- 使用new/delete的原始...
C++ 中文周刊 2025-11-15 第189期
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
本期文章没有赞助 好久没更新了,是在是太懒,空闲时间都用来打街霸6了,终于上了大师, 可以少玩一点了,给家人们更新周刊
家人们点点关注点点赞收藏打赏什么的,给点动力,没动力写不动
不过最近工作,愈发觉得语言更不重要了,AI这几年的进化太夸张了,生产力提升太高,涉及到学语言的地方真的少了
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
GCC已经加快更新了反射实现 https://gcc.gnu.org/pipermail/gcc-patches/2025-November/700733.html
重大生产力解放工具,总算来了
另外最近也在开会,没啥新东西
Trip report: November 2025 ISO C++ standards meeting kona
草药老师的纪要
文章
Trip report: Meeting C++ 2025
还是contract,没啥意思
Virtual Trip Report: WG21 Kona 2025
介绍了kona会上这个哥们感兴趣的提案
simd针对complex类型打补丁
理解std::constant_arg,以前是std::nontype_t
// libfoo.h:
int foo(std::function_ref<int(float)>);
// app.cpp:
void something() {
// 存储 lambda 对象 -> 间接调用:
foo([](float x) { return int(x * 0.5); });
// 函数指针为 nullptr -> 直接调用:
foo(std::constant_arg<[](float x) { return int(x * 0.5); }>);
}C++ Enum Class and Error Codes
枚举类型无法隐式转成bool
const auto ret = cpp::some_operation( ... );
if ( ret ) // 编译失败!enum class 无法隐式转换为 bool
if ( ret == cpp::result::Success ) // 可以工作,但更繁琐,我不喜欢打这么多字能不能外部封装一下?
enum class Result {
Success = 0,
SomeError,
SomeOtherError
};
// 编译失败!不能将 operator bool 定义为静态函数
inline explicit operator bool(Result r) { return r != Result::Success; } 当然AI可能会让你写两个感叹号
inline bool operator!(Result r) { return r == Result::Success; }
void foo()
{
const Result ret = cpp::some_operation( ... );
if ( !!ret )
{
// 处理错误
}
}太不合理了,我们考虑放到类型内部
struct Result {
enum class Value {
Success = 0,
SomeError,
SomeOtherError
} v;
explicit operator bool() const { return v != Result::Value::Success; }
};
void bar()
{
const Result ret = cpp::some_operation( /* ... */ );
if ( ret.v == Result::Value::SomeError ) {} // 可以工作
if ( ret.v == 42 ) {} // 编译错误
}还要写那么长的作用域前缀,怎么搞
内部inline一下
struct Result {
enum class Value {
Success = 0,
SomeError,
SomeOtherError
} v;
constexpr Result( Value x ) : v( x ) {}
constexpr explicit operator bool() const { return v != Result::Success; }
static constexpr Value Success = Value::Success;
static constexpr Value SomeError = Value::SomeError;
static constexpr Value SomeOtherError = Value::SomeOtherError;
// 对枚举中的每个值重复此操作
};
inline constexpr bool operator==( Result lhs, Result rhs ) { return lhs.v == rhs.v; }
inline constexpr bool operator!=( Result lhs, Result rhs ) { return lhs.v != rhs.v; }
void foo()
{
const Result ret = cpp::some_operation( /* ... */ );
if ( ret ) {} // 可以工作
if ( ret == Result::SomeError ) {} // 同样可以工作
if ( ret.v == 42 ) {} // 编译错误
}能用了,现在的问题是,脏活很多,要我说,就AI写就好了
AI看代码AI写代码,甩手掌柜,可能直接两个感叹号爱用不用
Automated Equality Checks in C++ with Reflection (C++26)
简单用c++反射实现相等比较
template<typename T>
bool compare_same(const T& a, const T& b) {
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if (!compare_same(a.[:mem:], b.[:mem:])) {
return false;
}
}
return true;
}这段代码的原理如下:
- ^^T 是一个反射运算符,它生成一个 std::meta::info 类型的值,代表类型 T 自身的元信息。
- std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()) 查询类型 T 的所有非静态数据成员,返回一个按声明顺序排列的反射值数组。其中 unchecked 访问上下文故意绕过访问控制(private/protected),使私有成员也能被比较。
- std::define_static_array 将这个运行时不可知的反射集合“固化”为一个编译期已知的静态数组,使其可被编译时循环遍历。
- template for 是一个编译时循环,它在编译阶段展开,对每个成员反射值 mem 执行一次迭代。
- a.[:mem:] 是拼接(splicing)表达式,它是 ^^T 的逆操作:^^ 进入“元宇宙”获取类型信息,而 [: :] 则从元宇宙返回到标准 C++ 语法世界,动态拼接出对成员的访问表达式。
完整代码 https://godbolt.org/z/er8b4GbE5
A prvalue is not a temporary 纯右值不是临时对象
纯右值是值,只有在需要转换的时候才会转成对象
听起来像废话
Efficient C++: The hidden compile-time cost of auto return types
头文件中的auto会使编译时间增加,可以通过 -ftime-trace 生成 trace文件分析
Looking at binary trees in C++
常规的二叉树写法,包括AI写的,都是双指针类型
class TreeNode {
public:
int data;
TreeNode* left;
TreeNode* right;
TreeNode(int value) : data(value), left(nullptr), right(nullptr) {}
};局部性很差,作者用数组存索引来改善
class BinaryTreeOptional {
private:
int root = -1;
std::vector<std::optional<TreeNodeOptional>> nodes;
size_t insertRecursive(int node, int value) {
if (node == -1) {
nodes.emplace_back(value);
return nodes.size() - 1;
}
auto node_value = nodes[node]->data;
if (value < node_value) {
nodes[node]->left = insertRecursive(nodes[node]->left, value);
} else if (value > node_value) {
nodes[node]->right = insertRecursive(nodes[node]->right, value);
}
return node;
}
void inOrderRecursive(std::optional<TreeNodeOptional>& onode) {
if (onode.has_value()) {
auto& node = onode.value();
if (node.left != -1)
inOrderRecursive(nodes[node.left]);
if (node.right != -1)
inOrderRecursive(nodes[node.right]);
}
}
public:
void insert(int value) {
root = insertRecursive(root, value);
}
void inOrderTraversal() {
if (root != -1)
inOrderRecursive(nodes[root]);
}
};
局部性好,元数据集中,数据量越多效果越好,性能提升30%+
Speeding up C++ functions with a thread_local cache
老代码中有那种遍历map还不好改的代码,如何加速访问,thread_local缓存index
感觉有点难评,能重写最好重写,不要补丁摞补丁
Using RAII to remedy a defect where not all code paths performed required exit actions, follow-up
有时候需要RAII+lambda模式,但如果接收方复制了lambda,由于RAII lambda被倒霉的执行两次,如何解决这个问题?
lambda用shared_ptr包一层
When Compiler Optimizations Hurt Performance
这是一个在AArch64 clang上的案例
代码
#include <bit>
int utf8_sequence_length(unsigned char lead_byte) {
switch (std::countl_one(lead_byte)) {
case 0: return 1;
case 2: return 2;
case 3: return 3;
case 4: return 4;
default: return 0; // 无效首字节
}
}性能不佳,原因是编译器帮忙优化成跳转表了
adrp x9, .Lswitch.table.utf8_sequence_length(unsigned char)
add x9, x9, :lo12:.Lswitch.table.utf8_sequence_length(unsigned char)
ldr w0, [x9, w8, uxtw #2]
...
.Lswitch.table.utf8_sequence_length(unsigned char):
.word 1
.word 0
.word 2
.word 3
.word 4使用 clang++ -O2 -fno-jump-tables --std=c++20可破
gcc没有生成跳转表,所以没有这个问题
函数是不对称的
在传统编程中,所有函数都必须返回“一个”值——即使它们可以接受任意数量的参数
这种“多入一出”的结构是不对称的。即使我们将 void 视为“一种返回值”(代表“无值”),也无法改变“返回值数量恒为一”的事实。
函数式语言(如 Haskell)通过柯里化(currying) 将所有函数转化为“一元输入、一元输出”,从而实现对称性。但 C++ 等主流语言缺乏对这种范式的原生支持,更习惯使用多参数函数。
另一种思路是将多个返回值打包成 tuple,但这繁琐且模糊了“返回一个结构”与“返回多个独立值”的语义边界
作者引入一个转换结构
template <callable ...Fs, typename ...Args>
auto call_by_need(some<Fs...>, some<Args...>);怎么实现,读者拿AI写一个
The case against Almost Always auto
auto隐藏了类型所有权,隐藏的信息被迫要好好命名,但命名很难
说的有点道理,不过现在AI写代码不auto,我这样的懒狗才auto
是情况而定,尽量不用,名字特长无法忍受可以auto
不过auto IDE也能帮你重构成具体的类型
Discovering observers - part 1
回顾一下观察者模式咋写,简单说,就是调用路径调用一下外部的钩子
外部钩子可以多个,也就涉及到观察者订阅取消管理。但大部分场景没那么复杂
贴一个代码例子 https://godbolt.org/z/Mv7YMM3aK
#include <iostream>
#include <string_view>
#include <vector>
template <typename Message>
class Subscriber {
public:
virtual ~Subscriber() = default;
virtual void update(Message message) = 0;
};
template <typename Message>
class Publisher {
public:
virtual ~Publisher() = default;
void subscribe(Subscriber<Message>* subscriber) {
std::cout << "Got a new subscriber\n";
_subscribers.push_back(subscriber);
}
void unsubscribe(Subscriber<Message>* subscriber) {
std::cout << "Someone unsubscribed\n";
std::erase(_subscribers, subscriber);
}
protected:
void notify(Message message) const {
std::cout << "Sending an update to " << _subscribers.size()
<< " subscriber(s)\n";
for (auto* const subscriber : _subscribers) {
notifyOne(subscriber, message);
}
}
private:
virtual void notifyOne(Subscriber<Message>* const,
Message message) const = 0;
std::vector<Subscriber<Message>*> _subscribers;
};
using SettingsMessage = std::pair<std::string, int>;
class SettingsSubscriber : public Subscriber<Settings...C++ 中文周刊 2025-08-11 第188期
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
最近有点忙,本周刊成功升级成月刊
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
how can I read more than 4GB of data from a file in a single call to ReadFile?
ReadFile参数 nNumberOfBytesToRead上限4G,大于4G的文件怎么读?自己拆成多个小于4G来读,历史原因,就这样不改了
std::format improvements (Part 2)
问题1: api割裂,std::format 支持编译期检查,之后运行时只好使用vformat
改进: 引入特化runtime_format
// 改进前
std::vformat(str, std::make_format_args(42));
// 改进后:更清晰的意图表达
std::format(std::runtime_format(str), 42);问题2 参数UB
std::string str = "{}";
auto args = std::make_format_args(path.string()); // 临时对象被移动
std::string msg = std::vformat(str, args); // 使用已销毁的临时对象改进: 只支持左值
问题3 char符号问题
for (char c : "\U0001F937") { // Unicode 字符 "🤷"
std::print("\\x{:02x}", c); // 可能输出 \x-10 或 \xf0
}改进: 特化char formatter
// 内部实现修改示例
template<>
struct formatter<char> {
auto format(char c, auto& ctx) {
return formatter<unsigned char>::format(+c, ctx); // 显式转换为无符号
}
};问题4 换行符
改进
std::cout << std::format("{:?}", path); // "multi\nline"目前还没有编译器实现
Format your own type (Part 1)
自定义 偏特化formater
#include <format>
#include <iostream>
#include <string>
struct ProgrammingLanguage {
std::string name;
int version{0};
};
template <>
struct std::formatter<ProgrammingLanguage> {
std::formatter<std::string> _formatter;
constexpr auto parse(std::format_parse_context& parse_context) {
return _formatter.parse(parse_context);
}
auto format(const ProgrammingLanguage& programming_language,
std::format_context& format_context) const {
std::string output = std::format("{}{}", programming_language.name,
programming_language.version);
return _formatter.format(output, format_context);
}
};
int main() {
ProgrammingLanguage cpp{"C++", 20};
std::cout << std::format("{} is fun", cpp) << '\n';
}这只是简单能力,如何实现分析具体内容来格式化输出?比如我想根据指定的属性,来定义格式化
需要定制parse
template <>
struct std::formatter<ProgrammingLanguage> {
std::string _attributes;
constexpr auto parse(std::format_parse_context& parse_context) {
auto it = std::ranges::find(parse_context, '}');
_attributes = std::string(parse_context.begin(), it);
return it;
}
auto format(const ProgrammingLanguage& programming_language,
std::format_context& format_context) const {
auto out = format_context.out();
if (_attributes.empty()) {
out = std::format_to(out, "{}{}", programming_language.name,
programming_language.version);
return out;
}
for (auto n = 0u; n <= _attributes.size(); ++n) {
if (_attributes[n] == '%') {
switch (_attributes[++n]) {
case 'n':
out = std::format_to(out, "{}",
programming_language.name);
break;
case 'v':
out = std::format_to(out, "{}",
programming_language.version);
break;
case '%':
out = std::format_to(out, "%");
break;
}
} else {
out = std::format_to(out, "{}", _attributes[n]);
}
}
return out;
}
};这样就支持 %n %v
Format your own type (Part 2)
接着上面的改动,丰富一下parser
#include <format>
#include <optional>
#include <string>
struct ProgrammingLanguage {
std::string name;
int major_version{0};
std::optional<int> minor_version{};
std::optional<int> patch_version{};
};
template <>
struct std::formatter<ProgrammingLanguage> {
std::string _attributes;
constexpr auto parse(std::format_parse_context& ctx) {
auto it = std::ranges::find(ctx, '}');
_attributes = std::string(ctx.begin(), it);
return it;
}
auto format(const ProgrammingLanguage& pl, std::format_context& ctx) const {
auto out = ctx.out();
if (_attributes.empty()) {
return std::format_to(out, "{}{}", pl.name, pl.major_version);
}
for (size_t n = 0; n < _attributes.size(); ++n) {
if (_attributes[n] == '%') {
++n;
switch (_attributes[n]) {
case 'l':
std::format_to(out, "{}", pl.name);
break;
case 'v': {
std::format_to(out, "{}", pl.major_version);
if (auto m = pl.minor_version)
std::format_to(out, ".{}", *m);
if (auto p = pl.patch_version)
std::format_to(out, ".{}", *p);
break;
}
case 'm':
std::format_to(out, "{}", pl.major_version);
break;
case 'n':
std::format_to(out, "{}", *pl.minor_version);
break;
case 'p':
std::format_to(out, "{}", *pl.patch_version);
break;
}
} else {
std::format_to(out, "{}", _attributes[n]);
}
}
return out;
}
};Data alignment for speed: myth or reality?
在主流的Intel和64位ARM处理器上,数据对齐对性能的提升作用被高估。作者认为这属于微观优化,性能测试无明显差异
除非涉及:
- 原子操作/锁竞争
- SIMD指令密集型计算
- 极端内存带宽敏感场景
3 C++ Lambdas Myths
几个误解
- lambda不就是函数么?不是,lambda可以有状态,因为是对象,所以需要注意生命周期
- lambda和std::function不就是一个玩意儿么?lambda和function差异很大,function可能有堆分配,也可能有sso优化,小lambda用
std::function<\void\>可以,延迟敏感创建频繁应谨慎使用std::function - lambda性能很大我尽量不用?不大,编译器基本都能优化掉
There is a std::chrono::high_resolution_clock, but no low_resolution_clock
低精度时钟实现
// Windows 平台专用实现
struct cheap_steady_clock {
using rep = int64_t; // 支持负时间差
using period = std::milli; // 毫秒单位
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<cheap_steady_clock>;
static constexpr bool is_steady = true; // 保证单调递增
static time_point now() noexcept {
return time_point{ duration{ static_cast<int64_t>(GetTickCount64()) } };
}
};
// Linux 平台专用实现
struct cheap_steady_clock {
using duration = std::chrono::nanoseconds; // 保持纳秒类型兼容性
using rep = duration::rep;
using period = duration::period;
using time_point = std::chrono::time_point<cheap_steady_clock>;
static constexpr bool is_steady = true;
static time_point now() noexcept {
struct timespec tp;
if (0 != clock_gettime(CLOCK_MONOTONIC_COARSE, &tp))
throw system_error(errno, "clock_gettime failed");
return time_point(std::chrono::seconds(tp.tv_sec) + std::chrono::nanoseconds(tp.tv_nsec));
}
};我见过一种玩法,用一个线程更新时间戳,精度更低,不知道和这个比性能差异啥样子
都不如rdtsc更直接一些
Avoiding Undefined Behaviour with BoostTests and standard types
向std命名空间或其嵌套命名空间添加声明,在绝大多数情况下都属于未定义行为。
极少数例外情况(如为自定义类型特化std::hash)是被允许的
namespace std {
template <typename T>
std::ostream &operator<<(std::ostream &os, const optional<T> &value) {
if (value.has_value()) {
return os << "std::optional{" << value.value() << "}";
}
return os << "std::nullopt";
}
} // namespace std这么玩是UB
boost绕了一下
namespace boost::test_tools::tt_detail {
template <typename T>
struct print_log_value<std::optional<T>> {
void operator()(std::ostream &os, const std::optional<T> &value) {
if (value.has_value()) {
os << "std::optional{";
print_log_value<T>()(os, *value);
os << "}";
} else {
os << "std::nullopt";
}
}
};
} // namespace boost::test_tools::tt_detail总得有人干脏活
How can I wait until a named object (say a mutex) is created?
windows Mutex可以用做多个进程互斥工具,但是没有很好的区分崩溃推出/无响应/多进程同时启动下场景
推荐共享内存+版本号,或者com 组件
What happens if C++/WinRT is unable to resume execution on a dispatcher thread?
Being more adamant about reporting that C++/WinRT was unable to resume execution on a dispatcher thread
C++/WinRT 库提供了 winrt::resume_foreground()函数,用于在调度器的前台线程上恢复协程。它支持三种调度器类型:
- Windows.UI.Core.CoreDispatcher
- Windows.System.DispatcherQueue
- Microsoft.UI.Dispatching.DispatcherQueue
问题场景:当线程切换失败时会发生什么?
...
C++ 中文周刊 2025-07-13 第187期
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
熬夜看了EWC饿狼传说比赛,小孩曾卓君亚军,被那个卡恩压的动不了
比街霸6爱德煤球还离谱的无责任压制,气的睡不着,傻逼游戏难怪销量不行
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
上一期补充
qt支持告警 clazy warning: "c++11 range-loop might detach Qt container ,自动优化cow forrange问题
文章
Base64 for compression
非utf-8字符 比如中文字符串会转成八进制字符,膨胀率非常高
如果需要降低体积,可以考虑使用base64压缩,gcc15支持
.base64 "w6nDqcOpw6nDqcOpw6nDqQA="引入计算,降低体积,大家看需求取舍
Discover C++26’s compile-time reflection
演示了一个生成sql的代码
template<typename T>
consteval std::string generate_sql_columns() {
std::string columns;
bool first = true;
constexpr auto ctx = std::meta::access_context::current();
[:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):]
>> [&]<auto member>{
using member_type = typename[:type_of(member):];
if (!first) {
columns += ", ";
}
first = false;
// Get member name
auto name = std::meta::identifier_of(member);
columns += name;
};
return columns;
}
template<typename T>
constexpr std::string generate_sql_valuess(const T& obj) {
std::string values;
bool first = true;
constexpr auto ctx = std::meta::access_context::current();
[:expand(std::meta::nonstatic_data_members_of(^^T, ctx)):]
>> [&]<auto member>{
using member_type = typename[:type_of(member):];
if (!first) {
values += ", ";
}
first = false;
// Get member value
auto value = obj.[:member:];
// Format value based on type
if constexpr (std::same_as<member_type, std::string>) {
// Escape single quotes in strings
std::string escaped = value;
size_t pos = 0;
while ((pos = escaped.find('\'', pos))
!= std::string::npos) {
escaped.replace(pos, 1, "''");
pos += 2;
}
values += "'" + escaped + "'";
} else if constexpr (std::is_arithmetic_v<member_type>) {
values += std::to_string(value);
}
};
return values;
}
template<typename T>
constexpr std::string generate_sql_insert(const T& obj, const std::string& table_name) {
constexpr std::string columns = generate_sql_columns<T>();
std::string values = generate_sql_valuess(obj);
return "INSERT INTO " + table_name
+ " (" + columns + ") VALUES (" + values + ");";
}我之前用protobuf生成过sql,代码类似这样
template <class PB>
std::string ProtoToUpdateSql(PB pb, const std::string& table, const std::string& whereCondition) {
std::vector<std::string> updateFields;
// 使用反射获取所有字段
const google::protobuf::Descriptor* descriptor = pb.GetDescriptor();
const google::protobuf::Reflection* reflection = pb.GetReflection();
for (int i = 0; i < descriptor->field_count(); i++) {
const google::protobuf::FieldDescriptor* field = descriptor->field(i);
// 跳过没有设置值的字段
if (!reflection->HasField(pb, field)) {
continue;
}
std::string fieldName = field->name();
std::string value;
// 根据字段类型获取值
switch (field->cpp_type()) {
case google::protobuf::FieldDescriptor::CPPTYPE_INT32:
value = std::to_string(reflection->GetInt32(pb, field));
break;
case google::protobuf::FieldDescriptor::CPPTYPE_INT64:
value = std::to_string(reflection->GetInt64(pb, field));
break;
case google::protobuf::FieldDescriptor::CPPTYPE_UINT64:
value = std::to_string(reflection->GetUInt64(pb, field));
break;
case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE:
value = std::to_string(reflection->GetDouble(pb, field));
break;
case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT:
value = std::to_string(reflection->GetFloat(pb, field));
break;
case google::protobuf::FieldDescriptor::CPPTYPE_BOOL:
value = reflection->GetBool(pb, field) ? "TRUE" : "FALSE";
break;
case google::protobuf::FieldDescriptor::CPPTYPE_ENUM:
value = std::to_string(reflection->GetEnumValue(pb, field));
break;
default:
value = "'" + ExtraString(reflection->GetString(pb, field)) + "'";
break;
}
updateFields.push_back(fieldName + " = " + value);
}
// 如果没有字段需要更新,返回空字符串
if (updateFields.empty()) {
return "";
}
// 构建SQL语句
std::stringstream ss;
ss << "UPDATE " << table << " SET ";
for (size_t i = 0; i < updateFields.size(); i++) {
if (i > 0)
ss << ", ";
ss << updateFields[i];
}
ss << " WHERE " << whereCondition;
return ss.str();
}
大家可以比较一下差距
CUDA优化黑魔法:假装CUTLASS库
很简单的一个黑魔法,只要在你的函数名前加上cutlass_,假装是CUTLASS库,有可能获得一定的性能提升
Advanced NVIDIA CUDA Kernel Optimization Techniques: Handwritten PTX
手写PTX性能提升7%-10%,怎么写?
- 使用 cuda::ptx libcu++
- 使用 CUTLASS,尤其是看到前面那个抽象例子
文章后面展示了一个GEMM plus top_k and softmax的例子,不懂,就不贴了
Case study of over-engineered C++ code
代码走读一下
- 冗余的小接口函数,一个属性一个getter,还他妈是虚函数,不嫌累
- 一大堆的类设计(这个见仁见智)
群友投稿, string奇怪的比较表现
#include <iostream>
#include <string>
int main() {
std::string a;
std::string b;
a.push_back(64);
b.push_back(-28);
char c = 64;
char d = -28;
std::cout << (a < b) << std::endl; // output 1
std::cout << (c < d) << std::endl; // output 0
return 0;
}原因 string内部char trait cmp使用unsigned char进行比较
有点像strcmp,不知道是不是为了兼容这个逻辑
gcc已经实现编译期异常
感谢mm投稿,非常好的功能 异常开销越来越小
Once more about dynamic_cast, a real use case
发布版本升级动态库以来dynamic_cast进行版本升级
代码类似
// https://godbolt.org/z/TK9WM65n1
#include <iostream>
#include <memory>
// sdk
class InterfaceForSomeServiceBase{
public:
virtual ~InterfaceForSomeServiceBase() = default;
};
class InterfaceForSomeService_v1: public InterfaceForSomeServiceBase {
public:
virtual void featureA() = 0;
virtual void featureB() = 0;
};
class InterfaceForSomeService_v2 : public InterfaceForSomeService_v1 {
public:
virtual void featureC() = 0;
};
class InterfaceForSomeService_v3 : public InterfaceForSomeService_v2 {
public:
virtual void featureD() = 0;
};
class Server {
public:
void handle(std::unique_ptr<InterfaceForSomeServiceBase> clientImplementation) {
if (auto* p = dynamic_cast<InterfaceForSomeService_v3*>(clientImplementation.get()); p) {
p->featureA();
p->featureB();
p->featureC();
p->featureD();
} else if (auto* p = dynamic_cast<InterfaceForSomeService_v2*>(clientImplementation.get()); p) {
p->featureA();
p->featureB();
p->featureC();
} else if (auto* p = dynamic_cast<InterfaceForSomeService_v1*>(clientImplementation.get()); p) {
p->featureA();
p->featureB();
} else {
std::cout << "unhandled version\n";
}
}
};
// client
class ClientServiceImplementation : public InterfaceForSomeService_v2 {
public:
void featureA() override {
std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureA\n";
}
void featureB() override {
std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureB\n";
}
void featureC() override {
std::cout << "ClientServiceImplementation(InterfaceForSomeService_v2)::featureC\n";
}
};
// server
std::unique_ptr<InterfaceForSomeServiceBase> LoadFromDLL() {
return std::make_unique<ClientServiceImplementation>();
}
int main() {
std::unique_ptr<InterfaceForSomeServiceBase> clientImplementation = LoadFromDLL();
Server s;
s.handle(std::move(clientImplementation));
}不过这么玩的越来越少了
首先是二进制边界传染
为了规避这种传染问题,通过socket来搞,然后用version字段解决
60 Second Lesson: C++ Token Pasting Metaprogramming
真能编词儿
就是通过字符串注册模版来统一接口,类似static 工厂模式
看代码
#include <iostream>
#define CONCAT(A, B) A ## B
int FooFunctionOne() {
return 1;
}
int BarFunctionOne() {
return -1;
}
template<int (*Func)()>
class UsesTemplates {
public:
int CallFunction() {
return Func();
}
};
// Example: TEMPLATE_CLASS(Foo) -> UsesTemplates<&FooFunctionOne>
#define TEMPLATE_CLASS(PREFIX) UsesTemplates<&CONCAT(PREFIX, FunctionOne)>
int main() {
TEMPLATE_CLASS(Foo) fooClass; // Expands to UsesTemplates<&FooFunctionOne>
TEMPLATE_CLASS(Bar) barClass; // ...C++ 中文周刊 2025-06-22 第186期
h="30%">
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
最近有点忙,不过c++26有新进展了
资讯
标准委员会动态/ide/编译器信息放在这里
这里是前方记者Mick发来的报道
欢迎来到C++26的第七次会议,C++26 Wording Freeze is Here! 在半个月后所有提案加入草案之后,新的草案将作为C++26 CD(Committee Draft)刊发,本周期也将正式进入NB Ballot阶段。至此,C++26的所有新特性已经完全确定,剩下的9个月将全部被用在bugfix上。
本次会议共约200人参与,比上次少一些,依然是传统的线下:线上=2:1模式。
在通过的提案方面,本次共通过11篇语言提案和35篇库提案。两者都是本周期的最高值,而且质量一点不差,可以自信的说本次会议是整周期最重要的一次,也是标准变动最大的一次。
语言方面,最最重要的提案,也是全周期最重要的语言提案,自然是P2996静态反射。在这次会议之前,C++的反射能力是很弱的,只能通过TMP魔法去艰难地拿到类的一些有限的信息(比如聚合的成员数量等等)。在一次会议中,C++的反射能力通过P2996和它的4篇辅助提案的通过得到了超级史诗级增强。现在,我们可以直接使用^^T得到T(类型或者值)的反射对象(std::meta::info类型的值),然后通过极为丰富的API去查询之前想都不敢想的各类信息,比如类型名称(identifier_of(^^T)),成员数量(members_of(^^T, …).size()),甚至各类以前用type_traits写的东西现在都可以用API像调用函数一样使用了(比如add_const(^^T )就等价于add_const_t)。除此之外,上述members_of API还给了我们探查任意类的成员数量,每个成员的修饰符/类型/…等等等等…。这只是P2996提供的220个函数的一小部分,但已经足以展示它的巨大力量。这种能力让很多之前需要依赖宏的反射操作(比如enum to string,debug printer)成为几十行的简单程序。
需要注意的是,C++26的静态反射的主要关注点是在探测方面,对生成方面并没有什么能力(比如说不能注入成员函数,不能直接生成变量,…)。这主要是因为主流编译期的架构让编译期注入很难实现,即使是2996中唯一的生成相关函数——被严重限制能力的define_aggregate——也被证明是很难实现的。这个函数会替你完成一个聚合的定义——struct S; define_aggregate(^^S, …);实质上让你程序性的指定S的定义。虽然目前只能生成聚合(没有private成员支持),只能注入成员变量(没有成员函数/using alias/…支持),但是就这么简单的函数支持已经足以让我们实现大量充满想象力的功能了(比如named tuple)
P2996并不是本次会议的终点,4篇辅助提案也都相当重磅。最重要的是P3096函数参数反射,它让我们第一次拥有了探查函数的参数名的手段——parameters_of等API保证了这一点,也让我们得以生成诸如把函数参数打包到一个struct里还保证同样的成员名这类之前想都不敢想的操作。当然,作为一门参数名称可以随便写,两个声明名字可以完全不同的语言,3096也必须面对这一点。提案采取了最简单的做法——如果存在两个声明/定义名称不一致,那么parameters_of就是不合法的。在未来,或许可以推出[[canonical]]属性来标记某个函数的“真正”参数名,不过那就是以后的事情了。
同样重要的是P3394注解。注解本身是一个很简单的提案,一种新的属性[[=xxx]],用开头的等号标记,可以附着在几乎任何地方——类,成员,函数,…。真正重要的是相关的annotations_of API,让我们可以通过这个方法获得库可以阅读的注解。比如说我们可以标记一个类为[[=serializable]],然后库可以自动获取所有标记了这个注解的类然后用反射序列化它们。我们还可以用[[=options(…)]]标记某个成员来指导自动生成命令行参数的第三方库。注解给了我们不可忽略的属性,as they should be!
另外两篇附加提案没有那么重要,但依然是很有价值的附加提案。P3491 define_static_{string,object,array}提供了缺失非持续编译期内存分配的一个临时解决方案:让编译器直接生成一个静态数组。这提供了一定的误用可能(让static段可能变得很大),不过确实是目前解决这一问题的最好方案了。P3293 x.[: base :]则解决了2996的一个小疏忽:subobjects包括成员变量和父类,但是只有前者可以用x.mem语法访问。通过允许x.[: base :]来直接访问父类子对象,我们可以不再需要用两个循环+C cast(因为static_cast无法处理private继承)处理所有子对象。
不在4篇附加提案里的,但是依然和反射紧密相连的特性是P1306 Expansion Statement(template for)。这是一个极度坎坷的提案,曾经被批准进入C++20,但是在最后一刻被没时间了的Core扔出去了,23周期没有任何进展,就在眼看要miss 26的时候终于起死回生,在最后一次会议的最后一天赶上了26的末班车。template for是编译期版本的range-for loop,其执行过程是直接将循环体拷贝N次,而不是生成jmp,从而允许我们写template for (constexpr auto mem : …)这类纯编译期循环语句。虽然本身和反射无关,但是无数的反射程序都因为template for而变得更简单,更容易理解(遍历所有成员终于可以写普通循环了!)
除了反射相关提案,Core这次还通过了constexpr virtual继承这个看上去非常矛盾的提案,不过考虑到constexpr virtual函数已经有了,这也是顺理成章了。此外顺便把预处理器中的所有UB去除了,迈出了focus on安全性的第一步。
标准库方面,通过的提案也毫不逊色。最重磅的是7篇Senders/Receivers补充提案——众所周知,去年通过的S&R框架和C++20 Coroutine一样只提供了最基础的框架内容,而没有提供任何实质性的易用性进步,甚至连个hello world都写不出来。P2079 Parallel Scheduler解决了这一点——为标准提供了第一个标准化的调度器。通过较为宽松的wording给了实现很大的自由度来接入操作系统的调度器,同时避免了直接往标准中加入线程池可能导致的oversubscription问题,并且让P2300的hello world例子终于可以跑通了。除此之外,P3552 std::task则终于补全了Coroutine最基础的库类型之一(另一个是generator,至此终于最重要的两个类型都标准化了),给了一个最最基础的可以使用co_await的类型。task还和S&R框架做了深度融合,真正做到了Senders = Awaitable的承诺。除此之外,P3149 async_scope可谓是S&R基础框架之外最重要的特性,为一直宣传的结构化并发编程打下了坚实的基础,终于不用担心各类生命期问题了。
本次会议历程最坎坷的提案毫无疑问是P2988 optional<T&>。C++17周期std::optional标准化时,就要不要支持引用的optional曾经爆发过激烈的争吵,直接导致一些人心灰意冷去了WG14。主要的争吵点是optional<T&>是不是可以用T*代替,以及其operator=到底应该像原生引用一样assign-through还是像T*一样rebind。争吵是如此激烈以至于最后决定不支持optional<T&>,“留待后人解决”。8年后,随着and_then等便利函数的加入,optional<T&>相对T*的优越性被更多人认知,rebind也形成了共识成为了唯一推荐的实现方案,最终在13个revision之后这一缺失的功能终于重新被C++接纳。下一步就该expected<T&, E&>和variant<T&>了吧?
本次会议变动最大的提案无疑是P3179 Parallel Ranges Algorithm。C++17加入了pSTL,为几乎每一个标准STL算法加入了多线程支持(通过第一参数的执行策略,比如std::execution::par_unseq)。但是在C++20 Ranges的大潮中,只有普通的标准算法被Range化了,这些pSTL并没有被关注。现在3179终于解决了这一疏忽,通过将output iterator改为output range同时限制随机访问来解决了相关的概念问题;现在所有的Ranges算法也都可以传入执行策略了。不过还有最后一步没有走完(执行策略和S&R框架的整合),那个就得等以后了。
还有一个重要的优化组合是P3557+P3560,这两篇提案让S&R和反射的报错都通过编译期异常来实现,成为了这个新锐特性向前迈出的一大步。与传统的SFINAE实现相比,编译期异常有着好得多的错误信息和编译速度,在S&R这种极度依赖TMP的框架中真正能发挥出它的作用。希望两者的错误信息不要像Ranges那么难懂吧!
最后还有P2830 type_order_v这个老熟人,在上次会议plenary失败之后,这次卷土重来获得了成功。该type trait终于给了我们编译期一个确定的类型排序手段,能够给出任意两个类型之间的大小关系,对type set等TMP手段很有用。据说S&R的实现有了这个特性减少了几百行…
除了上面提到的所有这些,这次会议还通过了一大堆友好便利的小特性,比如views::indices(indices(n)等价于0, 1, 2, …, n - 1,即iota(0, n)加自动处理类型问题),4篇SIMD补充提案(最重要的一个是终于抛弃了std::datapar::simd这个惨绝人寰的命名,改用std::simd::vec和std::simd::mask;以及simd终于可以当Ranges遍历了),exception_ptr_cast(不调用rethrow而探测exception_ptr内部信息的手段),std::string::subview(终于!返回string_view的substr版本),constexpr shared_ptr,constant_wrapper(更直接的integer_constant,通过template避免写类型名),加入C23标准库特性等等。这绝对是一次收获满满的会议。十分遗憾的是,尽管付出了最大的努力,还是有25篇提案掉了出去,其中不乏重要的(比如let_async_scope和并发队列;它们只能等待C++29了。)
EWG方面,本次会议效率拉满review了54篇提案,通过了包括UB白皮书的最初版(Profile+Implicit Contract+UB Annex组合拳),概念的部分应用(比如range_of<same_as<int>>),让assert()默认调用Contracts,0o八进制字面量等特性。到这个阶段,Stage 2工作组已经开始大量review C++29特性,其中的一些重磅的已经开始有了进展(fiber_context,PM等),对未来充满期望吧!LEWG也不赖,在最后一刻让meta::exception继承了std::exception改正了这个缺憾,研究了constexpr pSTL,并且花了一天时间看zstring_view相关提案来试图提升C API的可操作性和安全性。最后还就std::nontype_t要不要使用constant_wrapper吵了一架(没结果)。他们最重要的通过的C++29提案可能是返回optional<T&>的map::get,终于!不用再忍受operator[]的奇怪行为了。
Stage 1工作组方面,在Nico的多次请求下,SG9终于通过了一个输入版本的views::filter,解决了caching行为带来的多个不直观行为,并且通过了views::null_term(自动生成\0结尾的char Range),views::take_before(前者更通用的版本,指定终止符),组成了第一批C++29适配器。下周期SG9的重点将是解决<numeric>算法的Range化,从而终于迎来可用的ranges::sum。SG7设计了define_aggregate的属性支持,和属性反射一起组成了新一批injection特性,并且通过了consteval value希望一举解决非持续内存分配的主要矛盾。LEWGI则开始了新的重大提案——单位系统框架的review,有希望成为C++29的重大特性之一。
另外还有一些trip report,比如
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
文章
Reflection for C++26!!!
群友ykiko的文章,非常不错
Compiler Explorer: An Essential Kernel Playground for CUDA Developers
godbolt支持nvcc了,链接 https://godbolt.org/z/q6dPT7vsW
如何消费连续范围和连续迭代器
https://mysteriouspreserve.com/blog/2025/06/03/Consume-Contiguous-Iterators-and-Ranges/
感谢yexuanxiao投稿
jemalloc Postmortem
讲了 jemalloc演进历史,虽然现在只是维护态,感兴趣可以看看
Variadic class template arguments
科普文章,直接看代码
- 构造函数可变参数展开
#include <tuple>
template <typename... Ts>
class Storage {
public:
Storage(Ts&&... args) : elements(args...) {}
// some getters here
private:
std::tuple<Ts...> elements;
};
template <typename... Ts>
Storage(Ts&&... ts) -> Storage<Ts...>;
int main() {
auto myStore = Storage(41, 4.2, "forty two");
}
- 继承可变参数展开
经典的overload
template<typename... Ts>
struct Visitor : Ts... {
using Ts::operator()...;
};
auto v = Visitor{
[](int i){ std::cout << i << '\n'; },
[](float f){ std::cout << f << '\n'; },
[](const std::string& s){ std::cout << s << '\n'; }
};
v(42); // prints 42另外c++26可能支持可变参数友元函数
template<class... Ts>
class Foo {
friend Ts...;
};不用一个一个手写了
Learning about std::as_const, Qt containers and that detach might not mean what you think
qt容器有潜在cow可能性,即使你用const T&也阻止不了潜在的detach/cow
比如这段代码
for(const auto& reg:registrations)即使循环没改,但是仍然会创建registrastions副本避免竞争
得用std::as_const/qAsConst把registrations包起来才可以
qt支持告警 clazy warning: "c++11 range-loop might detach Qt container ,自动优化cow forrange问题
Voronoi, Hashing and OSL
省流,换了个算法
- 传统哈希函数使用1997年的Jenkins Lookup3哈希(需位旋转rot和混合mix操作)
- 性能瓶颈:3D Voronoi需计算27个相邻网格单元的哈希值(3x3x3),原函数单次计算耗时高
- 代码冗余:为生成3D偏移需调用多次哈希(如hash_float3_to_float3内部调用3次哈希)
- 优化方案 采用PCG3D哈希(Jarczyński & Olano, 2020论文)
为什么 Windows 还要有 Interlocked 函数,当我们已经有 std::atomic 了?
std::atomic来得太晚,以前的东西也还能用,就留着了
Snapshot Analysis for C++
想法挺好
不过我在github上见到过魔改gdb/jemalloc来支持分析的。比较简单,他这个功能差很多
Not a template error after all...
template<class ...Args >
QSqlQuery exec_sql(const QString& sql, Args... args)
{
QSqlQuery query;
query.prepare(sql);
Q_ASSERT(query.boundValues().size() == sizeof...(args));
bind_value(query,0, args...);
if(!query.exec() && query.lastError().isValid())
qWarning() << query.lastError().text()<< query.lastQuery();
return query;
}Qassert报错,但是却没检查出sql有啥问题,实际是是prepare报错,导致后面assert报错,掩盖了真实的问题
修复就是给prepare加上判断
template<class ...Args>
QSqlQuery exec_sql(const QString& sql, Args... args) {
QSqlQuery query;
if (!query.prepare(sql)) {
qWarning() << "PREPARE FAIL:" << query.lastError().text()
<< "| SQL:" << sql
<< "| Driver:" << query.driver()->lastError().text();
} else {
Q_ASSERT(query.boundValues().size() == sizeof...(args));
bind_value(query, 0, args...);
if (!query.exec() && query.lastError().isValid())
qWarning() << "EXEC FAIL:" << query.lastError().text();
...C++ 中文周刊 2025-06-02 第185期
公众号
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
资讯
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
不过感觉实现有点简单,不如这个 https://github.com/YimingQiao/Fastest-Bloom-Filter
boost新增接口库 Boost.OpenMethod
看下代码
https://github.com/jll63/Boost.OpenMethod/blob/master/examples/slides.cpp
感觉有点复杂
上一期评论
Declaring a friendship to self 什么时候需要friend 自身
Q ni 提问 作者懂不懂什么叫声明自身为友元, 根本没有这种东西
Q Da'Inihlust 和t就是不同类型 别人写的不对,你转发不解释,事实就是转发不负责
A: 接受批评,以后会AI审稿
文章
The Road to Flux 1.0
为了贴近标准库语法,使用operator | 尽管编译报错各种难看
另外就是引入iterable概念 想努力进29
另外这个Flux还是做了一些涉及规避view::filter的缺点的
Raw loops for performance
裸循环不比range慢(直觉结论)
The case of creating new instances when you wanted to use the same one
一段抽象代码
Widget GetWidget() {
if (!m_widget) {
m_widget = std::make_unique<Widget>();
}
return m_widget.get(); // 这里有问题
}由于Widget有接受bool的构造函数,又没有标记explicit,这里返回指针被转成true了
等价于
return Widget(m_widget.get() != nullptr); 要么改返回值,要么构造函数加explicit抓到这个问题
Concepts vs type traits
简单来说就是concepts更干净更省,type traits描述能力太弱,一层叠一层
Compiler Explorer and the Promise of URLs That Last Forever
godbolt短链走google跳转,不幸的是google短链跳转要关了,作者正在抢救
Three types of name lookups in C++
| 查找类型 | 触发形式 | 搜索范围 | 适用场景 |
|---|---|---|---|
| 限定名称查找 Qualified | a::b::x |
仅 :: 左侧指定作用域 |
精确控制符号来源 |
| 非限定名称查找 Unqualified | x |
由内向外逐层作用域 | 局部变量、类成员、全局量 |
| 参数依赖查找 (ADL) | func(arg) |
常规作用域 + 参数类型命名空间 | 函数调用(尤其运算符) |
What Is the Value of std::indirect T
不是 optional,是帮助move的堆上T,虽然用起来有点像,
if (!i.valueless_after_move())
std::cout << *i;An optimizing compiler doesn’t help much with long instruction dependencies
hash表查询类似
for (size_t i { 0ULL }; i < pointers.size(); i++) {
sum += vector[pointers[i]];
}链表遍历类似
while(p < total_accesses) {
sum += list[idx].value;
idx = list[idx].next;
if (idx == node_t::NULLPTR) {
idx = 0U;
}
p++;
}高ILP场景:编译器优化仍有价值(如3倍加速),但需结合内存访问优化。
低ILP场景:需重构算法(如改链表为数组)打破依赖链,而非依赖编译
| 场景 | O3比O1指令减少比例 | O3比O1最大加速比 | 主要瓶颈 |
|---|---|---|---|
| 随机内存访问(高ILP) | 10:1 | 3倍 | 内存带宽 |
| 链表遍历(低ILP) | 4.6:1 | ≤2.12倍 | 指令依赖链 |
std move Is Not Free
简单来说,就是move的用法以及收益
典型两个场景
局部变量返回
// 错误!阻止 NRVO 优化,强制降级为移动
return std::move(local_obj);
// 正确:允许编译器优化(直接构造或自动移动)
return local_obj; 类成员用完就丢
class Widget {
std::vector<int> data_;
public:
auto getData() {
return std::move(data_); // 必须!避免拷贝
}
};Python Performance: Why 'if not list' is 2x Faster Than Using len()
if not mylist 比 if len(mylist) == 0 快,两者的指令不一样,一个是LOAD_GLOBAL, TO_BOOL,一个是 LOAD_GLOBAL, LOAD_FAST, CALL, LOAD_CONST, COMPARE_OP
开源项目介绍
- asteria 一个脚本语言,可嵌入,长期找人,希望胖友们帮帮忙,也可以加群753302367和作者对线
C++ 中文周刊 2025-05-26 第184期
公众号
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
本期文章由 赞助老爷 赞助 在此表示感谢
资讯
标准委员会动态/ide/编译器信息放在这里
五月邮件列表出炉 mailing2025-05 pre-Sofia
yexuanxiao 推荐 Tightening floating-point semantics for C++
这个提案里面详细分析了C和C++标准目前对浮点数如何表示和产生何种结果,以及目前C和C++标准的不足之处。另外大部分编程语言在这方面要比C++现有规范模糊的多。如果需要准确的进行浮点数编程并严格处理所有错误情况,目前来说这应该是最好的索引资料。
另外Lancern有他的阅读评述,这里转载如下
5 月份的 WG21 提案已经出炉了。没有什么特别重大的提案,下面是一些频道主觉得有意思的、之前没发过的小改进。
P2927R3: Inspecting exception_ptr
标准库提案。目前要想访问 std::exception_ptr 指向的异常对象,需要先利用 std::rethrow_exception 抛出异常然后再在原地利用 catch 块接住。这个提案新加一个 std::exception_ptr_cast 函数,可以直接获取这个异常对象。P3411R2: any_view
标准库提案。std::ranges::any_view 是一个类型擦除的 std::ranges::view ,元素类型为 T 。在需要做实现隐藏的时候会很有用,另外也不需要在编译消息里面面对巨长的类型名了。P3655R1: zstring_view
标准库提案。新加一个 std::zstring_view ,类似于 std::string_view 但是确保引用的字符串是以 0 字符结尾的。在和 C API 交互时很有用。P3668R1: Defaulting Postfix Increment and Decrement Operations
核心语言提案。允许将 operator++(int) 和 operator--(int) 实现为 =default ,编译器会利用 operator++() 和 operator--() 为后置自增自减运算符生成定义。P3686R0: Allow named modules to export macros
核心语言提案。允许 C++20 模块导出预处理宏供模块用户使用。两个关键点:1) 模块作者可以控制导出哪些宏;2) 模块用户可以控制导入哪些宏。
p.s. 这个提案明显还不成熟,有不少 corner case 没有解决,短时间内很难看到进标准的希望。P3701R0: Concepts for Integer types, not integral types
核心语言+标准库提案。终于有人开骂了,目前在标准的表述里面 integral type 不仅仅包含了通常意义上的整数类型,还包含了 bool / char / wchar_t / char8_t / char16_t / char32_t 这些理论上并不是整数的类型。这个提案提出> 新加一个 integer type 的概念,这个概念只包含通常意义上的有符号或无符号整数类型。在此基础上再定义一个对应的 std::integer concept 。
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
boost网站 https://www.boost.org/ 更新了新UI
183期补充
前一期讨论了optional<T*>的不合理之处,本周刊一贯主张 不建议使用optional<T*>, 使用wrapper std::expected std::variant代替
这里加入一些读者的反馈
Q问题 A是作者回答
idingdong 评论 optional<T*> 例子不够明显 以及optional<T*>的使用有一定道理,不应该一刀切
A: 我用ai生成了一个 https://godbolt.org/z/159Mx3Yao
A: optional<T*>是三层状态的压缩, 为了整洁非要用这个也不是不可以, 我这里是有了一点标题党
空明流转 评论 比如说做硬件的时序模拟,打空拍和不打拍子和打有东西的拍子其实是三种不同的状态
这个时候optional<T*> 就是有意义的, 可以使用不同的方案做flattern
Q: iDingDong评论 没有体现出坏实践坏在哪里
A: 这个坏实践的例子就是分不清nullopt和nullptr 访问*报错
Q: 小鸡快跑评论 这optional<T*>不还是没用,和coroutine_handle一样搞个noop_handle不就行了
A: 实际上语义不一样,特化有点麻烦,尤其是已经有了这么多代码,有ABI Break
Q: 伤心哈 感觉不如optional<optional>
A: 想气死我是吧
lh_mouse 评论 这个例子明显不应该用 optional,用 (char*) -1 表示空值
A: 特殊值哨兵也算是一种方式吧
文章
How to Split Ranges in C++23
#include <print>
#include <ranges>
#include <string_view>
int main() {
using namespace std::string_view_literals;
constexpr auto text = "C++ is powerful and elegant"sv;
for (auto part : std::views::split(text, ' '))
std::print("'{}' ", std::string_view(part));
}
可能你听说过lazy_split 单次/大量/流 lazy_split合适,split适合多趟处理的,帮助缓存
chunk, c++23
std::vector<int> data{1, 2, 3, 4, 5, 6, 7, 8};
for (auto chunk : data | std::views::chunk(3))
std::print("{}\n", chunk);
/*
[ 1 2 3 ]
[ 4 5 6 ]
[ 7 8 ]
*/chunk_by c++23
std::vector<int> values{1, 3, 5, 2, 4, 6, 7, 9, 8};
for (auto group : values | std::views::chunk_by([](int a, int b) {
return (a % 2) == (b % 2); // Same parity
})) {
std::print("size {}, {}\n", group.size(), group);
}
/*
size 3, [1, 3, 5]
size 3, [2, 4, 6]
size 2, [7, 9]
size 1, [8]
*/Declaring a friendship to self
什么时候需要friend 自身
模版类,偏特化访问内部对象
template <typename T>
class Wrapper {
private:
int secret = 42;
template <typename U>
friend class Wrapper; // Friend itself to allow specializations access
public:
void showSecret() {
std::cout << "Secret: " << secret << std::endl;
}
};
template <>
class Wrapper<int> {
public:
template <typename T> requires (!std::same_as<T, int>)
void accessSecret(Wrapper<T>& w) {
// Thanks to the friend declaration, this specialization can access private members
std::cout << "Accessing: " << w.secret << std::endl;
}
};嵌套类,内部类需要外部的东西,例子
class Outer {
public:
class Inner {
int secret = 42;
friend class Outer;
};
void revealSecret(const Inner& inner) {
std::cout << "Inner's secret is: " << inner.secret << '\n';
}
};Improve Diagnostics with std stacktrace
介绍 c++23的std::stacktrace以及 stack_runtime_error,和boost一样,不多说
引申一个assert优化实践,assert没有信息也没有堆栈,有了stacktrace,就可以糊一个assert
#define dynamic_assert(cond, fmt, ...) \
do { \
if (cond) break; \
throw stack_runtime_error(std::format(fmt, __VA_ARGS__)); \
} while (0)或者
#define dynamic_assert(...) dynamic_assert_impl(std::stacktrace::current(), __VA_ARGS__)
template<class... Args>
void dynamic_assert_impl(const std::stacktrace& st, bool cond, std::format_string<Args...> fmt, Args&&... args) {
if (!cond) {
throw stack_runtime_error(std::format(fmt, std::forward<Args>(args)...), st);
Variadic Switch
考虑变参 switch实现,代码大概类似这样
template <class Visitor, class Variant, std::size_t ... Is>
auto visit_impl(Visitor visitor, Variant && variant, std::index_sequence<Is...>) {
auto i = variant.index();
switch (i) { (case Is: return visitor(get<Is>(variant));)... }
}首先是jumptable
template <auto> int h();
int mersenne(unsigned index){
//.L4:
constexpr static void* jtable[] = {
&&CASE_0, // .quad .L8
&&CASE_1, // .quad .L7
&&CASE_2, // .quad .L6
&&CASE_3, // .quad .L5
&&CASE_4, // .quad .L3
};
// cmp edi, 4
if (index > 4) {
goto DEFAULT; // ja .L2
}
// jmp [QWORD PTR .L4[0+rdi*8]]
goto* jtable[index];
CASE_0: //.L8:
return h<2>(); // jmp int h<2>()
CASE_1: //.L7:
return h<3>(); // jmp int h<3>()
CASE_2: //.L6:
return h<5>(); // jmp int h<5>()
CASE_3: //.L5:
return h<7>(); // jmp int h<7>()
CASE_4: //.L3:
return h<13>(); // jmp int h<13>()
//.L2:
// mov eax, -1
// ret
DEFAULT:
return -1;
}当然挨个手写不现实,考虑构造跳转表
template <int... Is>
int visit(unsigned index) {
using F = int();
constexpr static F* table[] = {&h<Is>...};
if (index >= (sizeof table / sizeof* table)) {
// 边界检查
return -1;
}
return table[index]();
}
https://godbolt.org/z/TWhWPEorv
在godbolt上它可能不会跳转而是直接变成调用
考虑递归visit 这个效果好
template <auto V>
int h();
template <typename F, typename V>
decltype(auto) visit(F&& visitor, V&& variant) {
constexpr static auto max_index = std::variant_size_v<std::remove_cvref_t<V>>;
constexpr static auto table = []<std::size_t... Idx>(std::index_sequence<Idx...>) {
return std::array{+[](F&& visitor, V&& variant) {
return std::invoke(std::forward<F>(visitor),
__unchecked_get<Idx>(std::forward<V>(variant)));
}...};
}(std::make_index_sequence<max_index>());
const auto index = variant.index();
if (index >= table.size()) {
// boundary check
std::unreachable();
}
return table[index](std::forward<F>(visitor), std::forward<V>(variant));
}使用初始化列表+表达式折叠
template <int... Is>
int visit(unsigned index) {
int retval;
std::initializer_list<int>({(index == Is ? (retval = h<Is>()), 0 : 0)...});
return retval;
}但gcc不行,引入短路+表达式折叠,可以
template <int... Is>
int visit(unsigned index) {
int value;
(void)((index == Is ? value = h<Is>(), true : false) || ...);
return value;
}要使前面介绍的折叠表达式技巧泛化为通用的 visit 函数,需要解决几个问题
确定返回类型
首先需要确定访问者函数的返回类型。由于标准要求对所有变体选项的访问者必须返回相同类型,因此我们可以简单地检查第一个选项的返回类型:
template <typename F, typename V>
using visit_result_t = std::invoke_result_t<F, std::variant_alternative_t<0,
std::remove_reference_t<V>>>;有了返回类型,我们就可以设置通用的 visit 函数:
template<typename F, typename V>
constexpr auto visit(F&& visitor, V&& variant){
using return_type = visit_result_t<F, V>;
constexpr auto size = std::variant_size_v<std::remove_cvref_t<V>>;
return visit_impl<return_type>(std::make_index_sequence<size>(),
std::forward<F>(visitor),
std::forward<V>(variant));
}处理返回 void 的visitor
由于无法创建 void 类型的变量,需要为返回 void 的visitor特殊处理:
template <typename R, typename F, typename V, std::size_t... Idx>
requires (std::same_as<R, void>)
constexpr void visit_impl(std::index_sequence<Idx...>, F&& fnc, V&& variant) {
auto index = variant.index();
if(((index == Idx ? std::forward<F>(fnc)(get<Idx>(std::forward<V>(variant))), true
: false) || ...)){
// 找到
return;
}
std::unreachable();
}这个实现使用折叠表达式和短路逻辑来执行对应索引的访问者函数。
避免返回类型的默认构造要求
通常做法是使用...
