C++ 中文周刊 2025-05-26 第184期

周刊项目地址

公众号

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

qq群 753792291 答疑在这里

RSS

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

本期文章由 赞助老爷 赞助 在此表示感谢


资讯

标准委员会动态/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();
}

这个实现使用折叠表达式和短路逻辑来执行对应索引的访问者函数。

避免返回类型的默认构造要求

通常做法是使用 std::optional 包装返回值,但我们可以利用折叠表达式本身就能告诉我们是否找到了匹配的访问者,因此不需要 optional:

template <typename R, typename F, typename V, std::size_t... Idx>
constexpr auto visit_impl(std::index_sequence<Idx...>, F&& fnc, V&& variant) {
  auto index = variant.index();
  union Container {
    ~Container() = default;
    ~Container() requires (not std::is_trivially_destructible_v<R>) {}

    char dummy{};
    R value;
  } ret{};

  if(((index == Idx ? std::construct_at(&ret.value, 
                    std::forward<F>(fnc)(get<Idx>(std::forward<V>(variant)))), true 
              : false) || ...)){
    // 找到
    return ret.value;
  }

  std::unreachable();
}

这里使用了联合体(union)来存储返回值,并提供了两个析构函数处理不同的析构需求。

支持仅可移动的返回类型

最后,需要确保实现能处理访问者返回的仅可移动(move-only)类型的对象:

template <typename R, typename F, typename V, std::size_t... Idx>
constexpr auto visit_impl(std::index_sequence<Idx...>, F&& fnc, V&& variant) {
  auto index = variant.index();
  union Container {
    ~Container() = default;
    ~Container() requires (not std::is_trivially_destructible_v<R>) {}

    char dummy{};
    R value;
  } ret{};

  if(((index == Idx ? std::construct_at(&ret.value, 
                    std::forward<F>(fnc)(get<Idx>(std::forward<V>(variant)))), true 
              : false) || ...)){
    // 找到 -> value 是活动成员
    if constexpr (!std::is_copy_constructible_v<R>){
      return R(std::move(ret.value));  
    } else {
      return ret.value;
    }
  }

  std::unreachable();
}

这通过显式移动构造临时的 R 类型对象实现,但仅在返回类型不可复制时才这样做。

通过这些技巧,我们实现了一个完全通用的、能处理多种返回类型情况的 visit 函数,可用于访问 std::variant 中的值。

最佳指针标记 tag pointer方法总结

什么是指针标记?tag pointer

指针标记是一种将元数据编码到指针中的技术,使得指针除了存储地址外还能携带额外信息。

文章分析了以下五种标记指针的方法:

基准测试还包括了”胖指针”作为基线,用两个字来存储标记和指针。

基准测试主要包含两种类型:仅检查标记(Check tag)和获取数据(Get data)。测试在ARM M1 MacBook和Intel x86环境中进行。

标记0的测试

对齐位标记表现最好,因为编译器可以利用零值进行多种优化,特别是可以使用单一指令同时掩码和比较

由于标记为0,指针可以不需额外指令就被解引用

标记1-7的测试

一旦不使用标记零,各对齐标记方法的效率基本相同,不能使用单指令完成标记检查,需要先进行与操作再比较

解引用前需要掩码去除标记,增加额外指令

架构差异

在x86上,低字节标记比其他方法快得多,这是因为x86指令集可以通过单独的寄存器直接访问64位寄存器的低字节 在ARM上,访问低字节只能通过掩码实现

标记检查性能

高位标记方案在纯粹的标记检查中优于低位方案,优化器能够向量化这个操作,同时检查多个高位标记

标记版本显著快于基线版本,编译器能够自动向量化对连续范围标记的检查

函数调用影响

一旦参数数量超过特定大小,程序必须开始向栈溢出参数, 由于非标记值的大小是标记值的两倍,它们会更早达到这个条件

在批量操作中,更大的参数大小会导致相对于标记值的速度下降

标记消除

在解释器中,有时会标记指针后立即解标记, 希望编译器能够消除标记/解标记操作

只有基线方法能完全消除标记, 通过提示,编译器能够消除几乎所有标记操作,除了高字节标记

浮点数考虑

对于浮点数操作,NaN标记的优势在于不需要解引用指针获取浮点值。这种优势大小取决于缓存情况:

对于大量使用浮点数的数据结构,NaN标记总是最优选择

结论

内存通常是瓶颈,而非CPU指令:节省1-2条指令影响有限,单个缓存未命中可能导致CPU等待数百个周期

一般情况下性能相似:大多数方案都需要1条指令获取标记,1条指令解标记指针

标记数量差异:对齐标记只支持8个标记(64位机器上),而使用整个字节可获得256个标记

可移植性考虑:可能无法总是假设指针的高位未使用,对齐标记在几乎任何架构上都可使用

最终,没有一种”最佳”的指针标记方案,选择取决于具体需求、平台和优化重点。真实工程中的答案通常是”视情况而定”。

Returning several values from a function in C++ (C++23 edition)

省流 std::expected

template <std::integral int_type>
std::expected<int_type, std::string> divide(int_type a, int_type b) {
    if (b == 0) {
        return std::unexpected("Error: Division by zero");
    }
    return a / b;
}

Dividing an array into fair sized chunks

N个块,M个人,如何分配让每个块相差不大

// N is the total number of elements
// M is the number of chunks
// i is the index of the chunk (0-indexed)
std::pair<size_t, size_t> get_chunk_range_simple(size_t N, size_t M, size_t i) {
    // Calculate the quotient and remainder
    size_t quotient = N / M;
    size_t remainder = N % M;
    size_t start = quotient * i + (i < remainder ? i : remainder);
    size_t length = quotient + (i < remainder ? 1 : 0);
    return {start, length};
}

Constructing Containers from Ranges in C++23

C++23 中从范围构建容器

#include <vector>
#include <ranges>
#include <iostream>

int main() {
    const std::vector<int> data{1,2,3,4,5,6,7,8,9};

    auto result = std::vector<int>(
        std::from_range,
        data
        | std::views::filter([](int n){ return n % 2 == 0; })
        | std::views::transform([](int n){ return n * n; })
    );

    for (int x : result) std::cout << x << ' ';   // 4 16 36 64
}

Could AI Bots Generate a C++ Line like this one: i = 0x5f3759df – ( i » 1 );

这个是倒数平方根算法那个创造性代码。

float Q_rsqrt(float number)
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y  = number;
  i  = * ( long * ) &y;                       // evil floating point bit level hacking
  i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
  y  = * ( float * ) &i;
  y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
  // y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

  return y;
}

ai不会推导只会抄袭

Silly parlor tricks: Promoting a 32-bit value to a 64-bit value when you don’t care about garbage in the upper bits

场景

int64_t int32_to_64_garbage(int32_t i32)
{
    int64_t i64;
    __asm__("" :        // 不做任何事
            "=r"(i64) : // 在寄存器中产生结果
            "0"(i32));  // 从这个输入
    return i64;
}

只在gcc/icc上有效

就当没看见就好

视频

C++ Weekly - Ep 481 - What is Tail Call Elimination (Optimization)?

就是介绍尾递归,没啥新东西

开源项目介绍


上一期

本期

下一期

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