C++ 中文周刊 2026-03-21 第198期

周刊项目地址

公众号

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了

qq群 753792291 答疑在这里

RSS

欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言

本期文章没人赞助


资讯

标准委员会动态/ide/编译器信息放在这里

编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期

性能周刊

文章

Chromium’s span-over-initializer-list success story

Arthur O’Dwyer记录了C++26 P2447(给span加上从initializer_list构造的能力)在Chromium里的实际收益

这个特性允许span<const T>直接从{a, b, c}这样的初始化列表构造,不需要先建个临时vector或array。Chromium在2024年底给base::span加了同样的构造函数,顺带清理了一大堆调用点

典型的before/after:

// Before:先建vector,再传给CertCache
std::vector<scoped_refptr<const Cert>> certs(
    {kcer_cert_0, kcer_cert_1, kcer_cert_2, kcer_cert_3,
     kcer_cert_3, kcer_cert_2, kcer_cert_1, kcer_cert_0,
     kcer_cert_0, kcer_cert_2, kcer_cert_3, kcer_cert_1});
CertCache cache(certs);

// After:直接传
CertCache cache({kcer_cert_0, kcer_cert_1, kcer_cert_2, kcer_cert_3,
                 kcer_cert_3, kcer_cert_2, kcer_cert_1, kcer_cert_0,
                 kcer_cert_0, kcer_cert_2, kcer_cert_3, kcer_cert_1});
// Before:临时std::array
ASSERT_TRUE(ConfigureAppContainerSandbox(
    std::array<const base::FilePath*, 2>{&pathA, &pathB}));

// After:直接传
ASSERT_TRUE(ConfigureAppContainerSandbox({&pathA, &pathB}));

文章还总结了几个”失败案例”——主要是CTAD的缺失。比如span(box.type) == {'f','t','y','p'}过不了,原因是语法规定==后面不能跟braced-initializer-list(除co_yield和赋值运算符外)

有趣的是,这些调用点为了表达”两个元素的视图”,各自发明了三种不同的workaround(临时array、双括号、显式cast),一有了正确的语法,全都收敛到同一种写法了。语言就应该这样

省流:C++26之后span<const T>参数直接传{a, b}就行了

How many branches can your CPU predict?

Lemire测了现代处理器的分支预测器到底能记住多少条分支

测试套路:把同一段随机数序列重复跑,让CPU有机会”学习”分支模式,看多少分支能被完美预测

while (howmany != 0) {
    val = generate_random_value();
    if (val is odd) write to buffer;
    decrement howmany;
}

结果:

处理器 可完美预测的分支数
AMD Zen 5 30,000
Apple M4 10,000
Intel Emerald Rapids 5,000

Lemire:我又一次对Intel感到失望。AMD在这个benchmark上干得非常好

对做benchmark的人来说有个实际意义:小数据集测出来的结果可能完全不代表真实场景,因为CPU学会了你的测试数据。

BIO: The Bao I/O Coprocessor

bunnie(知名硬件黑客,写过《芯片设计》那本书)在设计自己的22nm SoC Baochip-1x时,造了个I/O协处理器BIO作为Raspberry Pi PIO的替代品

先研究PIO: 发现PIO虽然只有9条指令,但是典型CISC思路——每条指令带barrel shifter、wrap-around、FIFO管理、side-set……移植到FPGA后发现PIO用的逻辑单元比RISC-V CPU核还多,critical path是RISC-V的两倍

BIO的设计: 直接用PicoRV32(RV32E,16个寄存器),然后把r16-r31扩展成特殊功能寄存器:

x16-x19:8深度FIFO的头/尾访问,空时read-halt,满时write-halt
x20:snap-to-quantum,halt直到下一个时钟量子
x21-x26:GPIO读写/方向控制
x27-x30:事件寄存器,条件阻塞
x31:core ID + 时钟计数

面积对比:BIO 14597 cells vs PIO 39087 cells,BIO大约是PIO一半面积,时钟速率是PIO的4倍以上

代码用标准RV32E汇编写,甚至支持C(通过Zig的clang编译器),不需要学PIO那套私有指令集。三个核并行做DMA、SPI等外设协议

这是硬件设计文章,不是纯C++内容。不过思路很清晰,RISC vs CISC的经典权衡在I/O协处理器上的体现。感兴趣的可以点进去看看

Looking at Unity finally made me understand the point of C++ coroutines

Mathieu Ropert终于找到了coroutines在游戏里的具体用武之地

问题: 游戏里有很多跨帧的特效/行为,比如一个物体先左移一格,再右移四次,再旋转。写成state machine极其丑陋:

class TimeWarp {
    enum class State { Jump, StepRight, HandsOnHips, DoAgain };
    State _state = State::Jump;
    int _i = 0;
    Transform* _transform;
    bool operator()() {
        switch (_state) {
        case State::Jump:
            _transform->position.x -= 1.f;
            _state = State::StepRight;
            break;
        case State::StepRight:
            _transform->position.x += 0.2f;
            if (++_i == 4) { _state = State::HandsOnHips; _i = 0; }
            break;
        // ...
        }
        return false;
    }
};

用C++23 generator写:

std::generator<std::monostate> TimeWarp(GameObject& obj)
{
    // It's just a jump to the left
    obj.transform.position.x -= 1.f;
    co_yield {};

    // Then a step to the right
    for (int i = 0; i < 4; ++i) {
        obj.transform.position.x += 0.2f;
        co_yield {};
    }

    // Let's do the time warp again!
    for (int i = 0; i < 4; ++i) {
        obj.transform.Rotate(0.f, 90.f * i, 0.f);
        co_yield {};
    }
}

co_yield {}的语义是”本帧做到这里,下帧继续”。跟Unity的yield return null一毛一样——Unity当年也是因为没有await,用yield hack了这个语义,作者亲切地称之为”The Unity Hack”

调度器不到100行:

class effects_manager {
public:
    void add(std::generator<std::monostate> effect) {
        _effects.push_back(std::move(effect));
        _iterators.push_back(_effects.back().begin());
    }

    void run() {
        // 清理已完成的coroutine
        int first = 0;
        for (; first != _effects.size()
               && _iterators[first] != _effects[first].end(); ++first);
        if (first != _effects.size()) {
            for (int i = first; ++i != _effects.size(); ) {
                if (_iterators[i] != _effects[i].end()) {
                    _effects[first] = std::move(_effects[i]);
                    _iterators[first] = std::move(_iterators[i]);
                    ++first;
                }
            }
            _effects.erase(begin(_effects) + first, end(_effects));
            _iterators.erase(begin(_iterators) + first, end(_iterators));
        }
        // 推进所有特效一帧
        for (int i = 0; i < _effects.size(); ++i)
            ++_iterators[i];
    }

private:
    std::vector<std::generator<std::monostate>> _effects;
    using effect_iterator =
        decltype(std::declval<std::generator<std::monostate>>().begin());
    std::vector<effect_iterator> _iterators;
};

用法:effects.add(TimeWarp(obj)),主循环里调effects.run(),完活

文章末尾还给出了更纯粹的版本——让coroutine yield一个Draw对象,run()收集所有Draw对象,顺手还能parallel_for并行推进

这个use case比Fibonacci生成器实在多了,不用co_await也不用搞一套executor。做游戏/动画的同学强烈推荐

Serenely Fast I/O Buffer (With Benchmarks)

SereneDB团队造了个新的I/O buffer(sdb::message::Buffer),跟folly::IOBuf、absl::Cord等比了一下

设计: 本质是链表形式的chunk队列,chunk大小指数增长(bounded by min_growth和max_growth)。关键点:这是个SPSC(单生产者单消费者),只有一个atomic变量_send_end,lock-free极简

支持三种数据状态:

class Buffer {
public:
    Buffer(size_t min_growth, size_t max_growth,
           size_t flush_size = std::numeric_limits<size_t>::max(),
           std::function<void(SequenceView)> send_callback = {});
    void WriteUncommited(std::string_view data);
    void Write(std::string_view data, bool need_flush);
    void Commit(bool need_flush);
    [[nodiscard]] uint8_t* GetContiguousData(size_t capacity);
    template<typename Op>
    void WriteContiguousData(size_t capacity, Op op);
};

benchmark数据(AMD Ryzen 9 9950X + jemalloc):

场景 folly appender sdb buffer sdb快多少
1亿包,中位14B 887ms 884ms 0.28%
100万包,中位0.3KB 28ms 26ms 6.96%
100万包,中位2.6KB 366ms 234ms 36.09%
1万包,中位82KB 90ms 50ms 45.06%

大包场景下比folly快接近一半。SPSC限制有点强,但场景合适的时候很香

代码:https://github.com/serenedb/serenedb

C++26: Span improvements

Sandor Dargo总结了C++26给std::span带来的四个改进

P2447R6:span从initializer_list构造(上面Arthur那篇讲过了)

void take(std::span<const int> v);
take({1, 2, 3});  // C++26前报错,C++26后OK

P2821R5:span.at()

终于有bounds-checked访问了:

std::array<int, 4> arr = {1, 2, 3, 4};
std::span<int> s{arr};
s[10];    // undefined behaviour
s.at(10); // throws std::out_of_range

string、string_view、vector、array、deque都有at(),span一直是孤儿,现在补上了

P2833R2:Freestanding里的span/expected

嵌入式场景能用了(span::at()因为会throw,不进freestanding子集)

P3029R1:更智能的mdspan CTAD

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
// C++26后:deduced as span<int, 5>,保留编译期大小信息
std::span s(p, std::integral_constant<std::size_t, 5>{});

都是实用的小修小补,感兴趣的可以点进去看看

视频

Can Standard C++ Replace CUDA for GPU Acceleration? - Elmar Westphal - CppCon 2025

Elmar Westphal在Forschungszentrum Jülich做了15年GPU程序员,主要是分子动力学/微磁学模拟,CUDA老手一枚,讲的是用标准C++的execution policy做GPU编程

GPU编程的演化路径:

核心思路:你写的是标准C++,换个编译器flag就能跑在GPU上:

std::for_each(std::execution::par_unseq, data.begin(), data.end(),
    [](auto& x) { x = compute(x); });

nvc++ -stdpar=gpu编译,上面这段代码就会生成GPU kernel。不写CUDA、不写kernel,代码可以同时跑CPU和GPU

实际效果: 对很多workload,生成的GPU代码性能和手写CUDA相当。不是所有场景都适合(legacy代码、复杂内存访问模式有限制),但对新代码来说portability极高

这个话题隔一段时间就有人拿出来讲一次,但这次讲的人是实际用CUDA多年的人,可信度高一些。Slides: Back_to_the_Standard.pdf

“But my tests passed!” - Exploring C++ Test Suite Weaknesses with Mutation Testing - Nico Eichhorn - Meeting C++ 2025

Nico Eichhorn(Bosch)讲Mutation Testing,这个是比代码覆盖率更狠的测试质量评估手段

原理: 工具自动对你的代码做微小的”变异”(mutant):

// 原代码
if (x > 0) return true;

// 变异1:改运算符
if (x >= 0) return true;

// 变异2:翻转条件
if (x <= 0) return true;

// 变异3:直接返回
return true;

每个mutant跑一遍测试套件:

最终得到一个mutation score = killed / total,越高越好

为什么比coverage更有用: 100%覆盖率只说明每行代码被执行过,不说明你的断言足够强。Mutation testing直接验证测试能不能发现bug

演讲包含Bosch真实项目的case study。C++主流工具是mull(LLVM-based)

Building Bridges: C++ Interop, Foreign Function Interfaces & ABI - Gareth Williamson - Meeting C++ 2025

Gareth Williamson讲多语言项目里C++如何跟其他语言互操作

各语言的C++互操方案:

C++互操的特殊挑战:

最佳实践:用extern “C”包一层C接口,或者用专门的interop工具(cxx、pybind11)处理转换。Slides: https://slides.meetingcpp.com

Kristian Ivarsson: Casual, an open-source SOA platform

Stockholm C++ Meetup,Kristian Ivarsson介绍他们用C++写的分布式应用服务器Casual

Casual的定位: 类似Tuxedo/WebLogic那个年代的企业中间件,但是现代化、开源、UNIX风格,用C++写的

核心特性:

面向的场景是需要事务保障的大规模分布式系统,不是互联网那套HTTP+微服务,而是金融/电信那套更严肃的场景

GitHub: https://github.com/casualcore/casual

Upgrading Sea of Thieves From C++14 to C++20 Wasn’t Easy Here’s Why - Keith Stockdale - CppCon 2025

Rare的Keith Stockdale讲《盗贼之海》把C++标准从C++14升到C++20的完整经历,干货很多

代码规模:

项目 源文件数 代码行数
Sea of Thieves游戏项目 28,269 977,148
Engine项目 19,419 449,028
其他 579 56,057

加起来约150万行,每个release还在持续新增代码。在这规模上升级C++标准,心理建设得做好

升级的第一道坎:MSVC /permissive-

C++20支持的关键前提是先开启MSVC的/permissive-(标准符合模式)。这个flag让MSVC严格遵循C++标准,关掉各种历史遗留的非标准扩展

但问题是:MSVC默认不开这个flag,而且他们的代码已经依赖了大量非标准行为。于是第一步就是:

开 /permissive- → 一堆编译错误 → 修 → 重复

策略:拆成小changelists,每个只改一点,easier to review,出问题也容易回滚。全部修完花了约4个月

实际踩的坑(有slides代码为证):

坑1:Two-phase name lookup。MSVC历史上不支持正确的两阶段名字查找,模板体里的dependent name在定义时不查找只在实例化时查找,而/permissive-开启后才强制执行标准行为。于是一堆老模板代码炸了

坑2:C++20 operator== rewriting。C++20会自动为operator==生成reversed和symmetric版本,用来支持a == bb == a的统一处理。但如果代码里已经手写了对称的operator==,编译器看到a == b就有两个候选,ambiguous:

error C2666: 'bool MyStruct::operator==(MyStruct)' 2 overloads have similar conversions
note: C++20 says that these are ambiguous even though the second is a restatement of the first

要么删掉冗余的,要么显式用= default

坑3:各种非标准写法积累。十年老代码库里总有各种依赖MSVC扩展的代码,/permissive-统统揪出来

跨平台编译器加持: 他们用GCC、Clang、MSVC三套编译器一起编,互相补充,能发现只有某个编译器能抓到的问题

build时间对比(AMD Threadripper 3975W, 128GB RAM): 升级后build time没有变差——这是他们比较担心的问题,结果是all good

获得的新特性:

最终结论(slides原话):

150万行游戏代码,现役游戏,没有固定deadline,一边出feature一边搞升级,花了约4个月把/permissive-打开。不是什么”升级只要改一行flag”的爽文,是实实在在的工程案例

Slides: Changing_Cpp14_To_Cpp20.pdf

开源项目介绍


上一期

本期

下一期

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账