C++ 中文周刊 2025-12-05 第190期

周刊项目地址

公众号

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

qq群 753792291 答疑在这里

RSS

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

本期文章由 孙咖啡 赞助 在此表示感谢

悲报,我尝试了一下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。

编译器优化示例:

关键发现:当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标准化了日历类型,实现类型安全的日期操作,无需脆弱的整数算术。库引入了daysweeksyearsmonths的持续时间类型,以及14+种日历类型如yearmonthdayweekdayyear_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亿像素。

实际后果:

解决策略:

  1. 将虚拟元素缩小到约3倍用户屏幕大小
  2. 通过在用户滚动时动态调整元素来虚拟化内容
  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实现二叉树与使用原始指针手动内存管理以及基于向量索引方法的性能影响。

关键发现:

主要代码示例:

using rt_ptr = std::unique_ptr<TreeNode>;

struct TreeNode {
    int data;
    rt_ptr left;
    rt_ptr right;
};

rt_ptr insertRecursive(rt_ptr& node, int value) {
    if (node == nullptr)
        return std::make_unique<TreeNode>(value);

    if (value < node->data)
        node->left = insertRecursive(node->left, value);
    else if (value > node->data)
        node->right = insertRecursive(node->right, value);

    return std::move(node);
}

作者指出,尽管现代C++智能指针具有安全优势,但基于指针的方法由于内存管理开销,相比基于索引的解决方案固有地面临性能限制。

C++ Enum Class and Error Codes (三部曲)

这是一个三部曲系列,探讨了C++中使用enum class进行错误处理的各种方案。

Part 1: enum class的尴尬处境

问题很简单:enum class不能在布尔上下文中求值。你想写if (ret)来检查错误?抱歉,编译器说不行。

尴尬的解决方案1 - 双重否定(!!):

enum class Result {
    Success = 0,
    SomeError,
    SomeOtherError
};

inline bool operator!(Result r) { return r == Result::Success; }

if ( !!ret ) { // 处理错误 }

尴尬的解决方案2 - C++03风格包装器:

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;
};

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; }

Part 2: 寻找更优雅的方案

C++20的using enum可以减少一些重复,但仍然不能模板化。

作者提出两个更好的替代方案:

方案1:C++26 Contracts 对于像Vulkan这样的图形API,大部分错误其实是API误用,用contract检查前置条件更合适。

方案2:std::expected(C++23,笔者最推荐)

using Error = std::variant<io_error, gltf_error, VkResult>;

std::expected<Blob, Error> read_from_path( const std::filesystem::path& );
std::expected<GltfAsset, Error> parse_gltf( const Blob& );
std::expected<Model, Error> upload_to_gpu( const GltfAsset& );

Model expected_load_model( const std::filesystem::path& path )
{
    return read_from_path( path )
        .and_then( parse_gltf )
        .and_then( upload_to_gpu )
        .or_else( []( const Error& e ) -> std::expected<Model, Error> {
            std::visit( overload{
                []( io_error e ) { log( /* error */ ); },
                []( gltf_error e ) { log( /* error */ ); },
                []( VkResult e ) { log( /* error */ ); } }, e
            );
            return {};
         } ).value();
}

代码可读性大大提升?

Part 3: 异常的逆袭

作者挑战了”异常性能差”的传统观念,引用Khalil Estell的研究表明现代x64/ARM64使用表驱动的异常处理,golden path几乎没有开销。

关键区分:

最简洁的代码:

Model load_model( const std::filesystem::path& path )
{
    return upload_to_gpu( parse_gltf( read_from_path( path ) ) );
}

虽然C++异常没有Java那样的编译器强制文档化,但对于可恢复错误,异常确实比std::expected或枚举返回值更简洁。关键是要有原则地使用,而不是把异常当成万能药。

Time-based Universally Unique Identifiers (UUIDs v7)

UUID发展到v7,终于解决了数据库索引的大麻烦。

各版本UUID对比:

版本 方法 问题
1 时间戳 + MAC 暴露设备位置/时间;MAC地址冲突
3, 5 基于名称的哈希 使用破损/过时的加密算法
4 随机 乱序;数据库性能差
7 时间 + 随机 解决了排序和索引问题

核心设计: 最高48位是Unix时间戳(毫秒精度),其余是随机位。这创造了按时间顺序排列的ID,对数据库索引友好。

PostgreSQL基准测试显示v7在插入时间和索引大小上都显著优于v4。

C++实现示例:

using uuid_t = std::array<uint8_t, 16>;

using uuid_t = std::array<uint8_t, 16>;

uuid_t generate_uuidv7()
{
   uuid_t bytes{};

   // get current Unix timestamp in milliseconds
   auto now = std::chrono::system_clock::now();
   uint64_t ms_since_epoch =
      std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();

   // fill with random bytes
   std::random_device rd;
   std::mt19937_64 gen(rd());
   std::uniform_int_distribution<uint64_t> dist;
   uint64_t rand_hi = dist(gen);
   uint64_t rand_lo = dist(gen);

   for (int i = 0; i < 8; ++i) bytes[i] = (rand_hi >> ((7 - i) * 8)) & 0xFF;
   for (int i = 0; i < 8; ++i) bytes[8 + i] = (rand_lo >> ((7 - i) * 8)) & 0xFF;

   // overwrite the first 6 bytes with timestamp (big-endian)
   bytes[0] = (ms_since_epoch >> 40) & 0xFF;
   bytes[1] = (ms_since_epoch >> 32) & 0xFF;
   bytes[2] = (ms_since_epoch >> 24) & 0xFF;
   bytes[3] = (ms_since_epoch >> 16) & 0xFF;
   bytes[4] = (ms_since_epoch >> 8) & 0xFF;
   bytes[5] = (ms_since_epoch) & 0xFF;

   // set version (UUIDv7 = 0b0111)
   bytes[6] = (bytes[6] & 0x0F) | 0x70;

   // set variant (RFC 4122)
   bytes[8] = (bytes[8] & 0x3F) | 0x80;

   return bytes;
}

std::string to_string(const uuid_t& uuid)
{
   return std::format(
      "{:02x}{:02x}{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}-"
      "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
      uuid[0], uuid[1], uuid[2], uuid[3],
      uuid[4], uuid[5],
      uuid[6], uuid[7],
      uuid[8], uuid[9],
      uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]
   );
}

.NET 9+更简单:

var guid = Guid.CreateVersion7();

如果你的数据库主键还在用UUID v4,是时候考虑升级了。

Universally Unique Lexicographically Sortable Identifiers (ULIDs)

ULID是UUID v7的”表亲”,128位长度相同,但编码方式更友好。

关键特性:

结构: 最高48位是UNIX时间戳(毫秒),剩余80位随机化。

C++完整实现:

struct ulid_t
{
public:
   static ulid_t generate()
   {
      ulid_t ulid;

      uint64_t ts = now_ms();
      ulid.data[0] = (ts >> 40) & 0xFF;
      ulid.data[1] = (ts >> 32) & 0xFF;
      ulid.data[2] = (ts >> 24) & 0xFF;
      ulid.data[3] = (ts >> 16) & 0xFF;
      ulid.data[4] = (ts >> 8) & 0xFF;
      ulid.data[5] = (ts >> 0) & 0xFF;

      static thread_local std::mt19937 rng{ std::random_device{}() };
      for (size_t i = 6; i < 16; ++i)
      {
         ulid.data[i] = static_cast<uint8_t>(rng());
      }

      return ulid;
   }

   std::string string() const
   {
      return encode_base32(data);
   }

   explicit operator std::string() const
   {
      return encode_base32(data);
   }

private:
   std::array<uint8_t, 16> data{};

   static uint64_t now_ms()
   {
      using namespace std::chrono;
      return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
   }

   static constexpr char ENCODING[32] =
   {
       '0','1','2','3','4','5','6','7','8','9',
       'A','B','C','D','E','F','G','H','J','K',
       'M','N','P','Q','R','S','T','V','W','X',
       'Y','Z'
   };

   static std::string encode_base32(const std::array<uint8_t, 16>& bytes)
   {
      std::string out(26, '0');
      uint32_t bitpos = 128 + 2;

      for (int i = 0; i < 26; ++i)
      {
         bitpos -= 5;
         uint32_t value = extract_5bits(bytes, bitpos);
         out[i] = ENCODING[value];
      }

      return out;
   }

   static uint32_t extract_5bits(const std::array<uint8_t, 16>& bytes, uint32_t bitpos)
   {
      uint32_t byteIndex = bitpos >> 3;
      uint32_t startBit = bitpos & 7;

      uint32_t v = (bytes[byteIndex] << 8) |
         (byteIndex + 1 < 16 ? bytes[byteIndex + 1] : 0);

      v >>= (11 - startBit);
      return v & 0x1F;
   }
};

// 使用示例
ulid_t ulid = ulid_t::generate();
std::println("{}", static_cast<std::string>(ulid));

示例ULID:

ULID相比UUID v7的优势在于编码更紧凑(26字符 vs 36字符)且没有横线,在URL中更美观

但UUID v7有标准库支持(.NET 9+),ULID需要第三方库。选哪个?看你更在意标准还是美观。

Time in C++: Understanding chrono (三部曲)

这是一个系列文章,深入探讨C++的<chrono>库。

Part 1: 三大支柱

chrono的三个核心概念:

  1. Durations(时长) - 时间间隔
  2. Time points(时间点) - 相对于时钟纪元的时刻
  3. Clocks(时钟) - 提供time_point的来源,有纪元和tick rate

Duration结构:

template<class Rep, class Period = std::ratio<1>>
class duration;

常用别名:

std::chrono::nanoseconds  // 十亿分之一秒
std::chrono::milliseconds // 千分之一秒
std::chrono::seconds, minutes, hours

代码示例1 - 类型安全的Duration比较:

#include <chrono>
int main() {
    using namespace std::chrono_literals;
    constexpr auto d1 = 500ms;   // std::chrono::milliseconds
    constexpr auto d2 = 2s;      // std::chrono::seconds
    static_assert(d1 < d2);
    return 0;
}

代码示例2 - 显示当前时间点:

#include <chrono>
#include <iostream>
int main() {
    using namespace std::chrono_literals;
    std::chrono::time_point now = std::chrono::system_clock::now();
    std::cout << now << '\n';
}
// 输出: 2025-11-05 04:49:07.802540469

代码示例3 - 时间点相减:

#include <chrono>
#include <iostream>
int main() {
    using namespace std::chrono_literals;
    std::chrono::time_point now = std::chrono::system_clock::now();
    std::chrono::time_point now2 = std::chrono::system_clock::now();
    auto d = now2 - now;
    auto d2 = now - now2;
    std::cout << d << ' ' << d2 << '\n';
    return 0;
}
// 输出: 565ns -565ns

代码示例4 - 时间点偏移:

#include <chrono>
#include <iostream>
int main() {
    using namespace std::chrono_literals;
    std::chrono::time_point now = std::chrono::system_clock::now();
    std::chrono::time_point earlier = now - 10min;
    std::chrono::time_point later = now + 10min;
    std::cout << earlier << ' ' << now << ' ' << later << '\n';
    return 0;
}
// 输出: 2025-11-05 04:49:53 2025-11-05 04:59:53 2025-11-05 05:09:53

Part 2: system_clock - 墙上的钟

system_clock测量”系统范围的挂钟时间”,锚定到Unix纪元(1970年1月1日)。

适用场景:

不适用场景:

代码示例1 - 获取当前时间:

#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    std::cout << "now: " << now << '\n';
    return 0;
}
// now: 2025-11-10 05:43:45.622822844

代码示例2 - 转换为time_t:

#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    std::time_t now_c = std::chrono::system_clock::to_time_t(now);
    std::cout << "Current time: " << std::ctime(&now_c);
    return 0;
}
// Current time: Mon Nov 10 05:45:35 2025

代码示例3 - 从time_t转换:

#include <chrono>
#include <iostream>

int main() {
    std::time_t t = std::time(nullptr);
    auto time_point = std::chrono::system_clock::from_time_t(t);
    std::cout << "time_point: " << time_point << '\n';
    return 0;
}
// time_point: 2025-11-10 05:47:33.000000000

代码示例4 - 时钟抽象模式(用于测试):

struct Clock {
    virtual std::chrono::system_clock::time_point now() const = 0;
    virtual ~Clock() = default;
};

struct RealClock : Clock {
    std::chrono::system_clock::time_point now() const override {
        return std::chrono::system_clock::now();
    }
};

struct FakeClock : Clock {
    std::chrono::system_clock::time_point provided{};
    std::chrono::system_clock::time_point now() const override {
        return provided;
    }
};

Part 3: steady_clock - 永不倒退的钟

核心特性: steady_clock是单调的——”永远不会倒退”。

用途对比:

方面 steady_clock system_clock
方向 总是增加 可能向前/向后跳
目的 测量经过时间 挂钟/日历时间
用例 性能测量、超时 人类可读的时间戳

基本用法:

auto start = std::chrono::steady_clock::now();
// ... 运行某些代码 ...
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed = end - start;
std::cout << "Elapsed time: " << elapsed.count() << " seconds\n";

代码示例2 - 时钟抽象模式:

struct Clock {
    using time_point = std::chrono::steady_clock::time_point;
    virtual time_point now() const = 0;
    virtual ~Clock() = default;
};

struct RealClock : Clock {
    time_point now() const override {
        return std::chrono::steady_clock::now();
    }
};

struct FakeClock : Clock {
    time_point current{};
    time_point now() const override { return current; }
    void advance(std::chrono::milliseconds delta) { current += delta; }
};

代码示例3 - 使用FakeClock进行测试:

FakeClock clock;
auto start = clock.now();
clock.advance(1s);
auto end = clock.now();
assert(end - start == 1s);

笔者:这三篇文章是chrono库的绝佳入门。记住一个简单原则:测量时长用steady_clock,显示时间用system_clock。如果你还在用time_tgettimeofday(),是时候拥抱现代C++了。

PSA: Enable -fvisibility-inlines-hidden in your shared libraries to avoid subtle bugs

这是一个踩坑血泪史。当你在共享库中使用-fvisibility=hidden但忘记加-fvisibility-inlines-hidden时,会遇到非常诡异的bug。

问题根源: 当公共类使用可见性属性标记时,该属性会覆盖类中所有成员的隐藏可见性,包括内联函数。这导致内联函数被导出并在库边界之间合并,根据其他库的编译方式产生不一致的行为。

问题示例(a.h):

#define A_API   __attribute__((__visibility__("default")))
#define A_CLASS __attribute__((__visibility__("default")))

A_API void Foo();

struct A_CLASS Vec3
{
    float x, y, z;

    float LengthSq() const
    {
        return x*x + y*y + z*z;
    }
};

a.cpp:

void Foo()
{
    std::println("{}", Vec3{-0.02146666, 0.0014069901, 0.0014069926}.LengthSq());
}

正确的解决方案:

#ifdef _WIN32
#  ifdef MYLIB_BUILD
#    define MYLIB_API __declspec(dllexport)
#  else
#    define MYLIB_API __declspec(dllimport)
#  endif
#  define MYLIB_CLASS
#else
#  define MYLIB_API   __attribute__((__visibility__("default")))
#  define MYLIB_CLASS __attribute__((__visibility__("default")))
#endif

最佳实践:

笔者:这种bug属于”不出则已,一出要命”的类型。症状诡异,排查困难。建议所有共享库项目立即检查编译选项,别等到生产环境爆炸再后悔。

Atomics in C++26?

C++26给<atomic>库新增了fetch_maxfetch_min操作,支持整数、指针和浮点类型。

核心功能: “原子地执行参数与原子对象值之间的std::max/min操作,并获取先前持有的值。”

接口:

fetch_max(T val, memory_order = memory_order_seq_cst)

实际应用示例(原子队列):

std::atomic_fetch_max(&queue.back, i)

笔者:这个功能从2016年提案到2026年标准化,等了整整10年。虽然目前主流编译器还没实现,但对于需要原子更新最大/最小值的场景(比如统计、监控),这是个很实用的补充。不过作者的LinkedIn调查显示大部分开发者很少用高级原子特性——看来这个功能注定是小众的。

ARM’s barrel shifter tricks

这篇是”编译器优化降临日历2025”系列的第5天,对比了x86的lea和ARM的桶形移位器。

ARM的秘密武器: 许多ARM指令可以在第二操作数上包含移位操作,一条指令搞定两件事。

代码示例:

mul_by_2(int):
  lsl w0, w0, #1    ; w0 = w0 << 1
  ret

mul_by_3(int):
  add w0, w0, w0, lsl #1  ; w0 = w0 + (w0 << 1)
  ret

mul_by_7(int):
  rsb r0, r0, r0, lsl #3    ; r0 = (r0 << 3) - r0
  bx lr

限制: ARM采用固定4字节指令格式,常数值(除mov外)不能直接作为操作数。

笔者:x86有lea,ARM有桶形移位器,各有各的优化秘籍。编译器比你我都懂这些门道,所以别再手动”优化”乘法了,写清晰的代码就好。

Addressing the adding situation

这是系列第2天,讲x86如何优化加法。

x86的限制: add指令遵循”两个操作数”模式,结果覆盖左操作数。不像ARM那样有独立的目标寄存器。

创意解决方案: 滥用lea(Load Effective Address)指令。lea本来是算地址的,但谁说算出来的地址一定要访问内存呢?

通过x86复杂的寻址模式(常数+寄存器+寄存器*标度因子),lea变成了一个真正的三操作数加法器。

好处:

笔者:lea堪称x86指令集的”瑞士军刀”。设计它的人可能没想到,这个算地址的指令最后成了做算术的利器。

Why xor eax, eax?

系列第1天,经典问题:为什么用xor eax, eax清零而不是mov eax, 0

两个层面的优化:

  1. 空间: xor eax, eax占2字节,mov eax, 0占5字节
  2. 执行: 现代x86 CPU识别”清零习语”,检测到该操作不依赖eax的前值,从执行队列中移除,实现零周期执行

代码对比:

# 优化版(-O2):
31 c0           xor eax, eax
c3              ret

# 未优化版(-O1):
b8 00 00 00 00  mov eax, 0x0
c3              ret

寄存器宽度细节: xor eax, eax会自动将整个64位rax清零(写入eax时自动清零上32位)。

笔者:这种优化已经是编译器的基本操作了,但了解背后的原理仍然很有趣。更有趣的是CPU居然能”删除”指令执行——这就是乱序执行和寄存器重命名的魔法。如果你看到代码评审中有人把xor eax, eax改成mov eax, 0说”更清晰”,请把这篇文章甩给他。

开源项目介绍


上一期

本期

下一期

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