Skip to content

img

败犬のC++每月精选 2025-07

1. switch enum,每个枚举值都有 return 但是 MSVC 报 not all control paths return a value

cpp
enum class E { A, B, C };

int foo(E e) {
    switch (e) {
        case E::A:
        case E::B:
        case E::C:
            return 1;
    }
}

可以在 default(或函数末尾)里写 std::unreachable()。如果 C++23 之前可以自己写一个:

cpp
[[noreturn]]
inline void unreachable() {}

虽然这是 MSVC 独有,但是事实上标准允许 enum 的值不在“命名枚举值”的范围内,还是要注意的(最好写 default return)。https://en.cppreference.com/w/cpp/language/enum.html 有记载:

Values of integer, floating-point, and enumeration types can be converted to any enumeration type by using static_cast. Note that the value after such conversion may not necessarily equal any of the named enumerators defined for the enumeration: ...

翻译:整型、浮点型和枚举类型的值可以通过使用 static_cast 转换为任何枚举类型。请注意,这种转换后的值不一定等于枚举定义的任何命名枚举器:...

2. C++ 和命名参数

现在 C++ 的语法允许函数声明之间的形参名不同,比如:

cpp
void foo(int a, int b);
void foo(int b, int a);

所以如果要支持命名参数,不能直接在现有语法上加,肯定要增加新的关键字或符号。现有方案“又不是不能用”。

《C++ 的设计与演化》是这么说的:

C++ 是面向对象的语言,所以决定不提供这个功能。你的参数如果需要这么写,那请打包成一个对象。

但是这和面向对象没什么关系(C++ 是多范式语言),正经人都不喜欢啰嗦的 FunBuilder.setA().setB().setC().build()(更新,这个不啰嗦,啰嗦的是 setter 定义)。


那么推荐做法是什么呢?指派初始化。

cpp
struct Args {
    int a;
    int b;
};

void foo(Args args);

foo({.a = 0, .b = 1});

这个指派初始化是 C++20 的,20 之前使用也没什么问题(编译器扩展)。

但是有个缺点是参数必须按顺序初始化。这个需求有一个 linter 下位替代,clang-tidy 的 bugprone-argument-comment。如果要允许乱序的话问题有点大,主要是求值顺序的问题(实参的求值顺序和形参初始化的顺序)。详见乱序指派初始化提案 https://wg21.link/p3405

最后还有个炫技的命名参数实现 https://godbolt.org/z/16EWPnxfT(看一乐)。

3. 状态管理,用全局变量实现容易堆屎山

大家的共识是函数参数传 context。

但是如果整个程序用同一个 context 也会堆屎山,要保证 context 被使用的范围足够小。

还有 context 的一个问题是成员的生命周期,什么时候有值,什么时候没有值。一般只能靠注释,没有别的好办法。

4. 变量名包含 __LINE__ 的宏

直接这么写是不对的:

cpp
#define macro(x) int x_##__LINE__ = 0

int main() {
    macro(x);  // int x____LINE__ = 0;
}

要给这 x 和 __LINE__ 套两个宏再拼接:

cpp
#define CONCAT1(x, line) x##line
#define CONCAT(x, line) CONCAT1(x, line)
#define macro(x) int CONCAT(x, __LINE__) = 0

int main() {
    macro(x);  // int x6 = 0;
}

5. 为什么虚表里的函数从第 2 项开始

https://godbolt.org/z/bqxvKGeTP

cpp
struct Base {
        virtual ~Base();
};
struct Child : Base {
        ~Child();
};
void foo(Base* obj) noexcept {
        [[assume(obj != nullptr)]];
        delete obj;
}
text
foo(Base*):
        mov     rax, QWORD PTR [rdi]
        jmp     [QWORD PTR [rax+8]]

第 1 项是 RTTI 项。

关了 RTTI 的话,第一个 RTTI 项 是 0。这是为了让 RTTI 不影响 ABI,没开 RTTI 和 开了 的链接到一起也没事,只要没访问 RTTI 仅虚函数调用是没问题的。

还有另外一个问题,为什么 RTTI 不和虚表放一起,而是间接寻址(类似指针)的方式访问?原因是一样的。如果 RTTI 信息和虚表放一起,开关 RTTI 会影响虚表布局。

6. const_cast 什么场景下必须用

一种是上游或下游不提供非 const 接口,需要手动转换。

还有 iterator 或 span 有时会遇到。例如写 hash_map find,会有返回 iterator 和 const_iterator 两个版本,const 版本可以直接 const_iterator{const_cast<T*>(this)->find(key)}

7. clang constexpr 过早实例化的问题

cpp
#include <memory>

struct B;

struct A {
    std::unique_ptr<B> b = nullptr;
};

struct B {};

这个代码在 clang -std=c++23 报错,把标准降到 20 不会。

这是 clang 的一个老问题,std::unique_ptr<B> 的析构函数在 C++23 变成了 constexpr,在 struct B {}; 前实例化(过早实例化),于是要求 B 是一个完整类型。这也能解释 20 前就不会报错。

有些编译器把函数模板实例化放到 TU(翻译单元)结束,也就是 struct B {}; 后,这样可以不报错。不过这是个 IFNDR (ill-formed no diagnostic required),实例化位置和顺序标准显然没有规定的。ref https://timsong-cpp.github.io/cppwp/n4659/temp.point#8

一个几乎一样的问题:https://github.com/llvm/llvm-project/issues/111185

但是还有一个现象是 std::unique_ptr<B> b = nullptr; 改成 std::unique_ptr<B> b{nullptr}; 就不会报错了。虽然两者选择的都是 unique_ptr(nullptr_t) 构造函数(为什么 = 没有复制,因为 C++17 强制复制消除),但是 clang 实现有细微区别,多了个隐式转换,{} 不接受类型窄化就不需要。具体还得看 clang 源码。

8. GCC/Clang 模板语境 lambda 是 dependent expression

cpp
#include <bits/stdc++.h>

template <typename T>
class Switch {
   private:
    T value_;

   public:
    explicit Switch(T value) : value_(value) {}

    template <typename U, typename Fn>
    Switch& Case(Fn&& fn) {
        return *this;
    }
};

int main() {
    // Regular lambda
    auto regularLambda = [](int value) {
        Switch<int>(value)
            .Case<int>([]() {})     // OK: no template keyword needed
            .Case<float>([]() {});  // OK: no template keyword needed
    };

    // Generic lambda
    auto genericLambda = [](auto value) {
        Switch<int>(value)
            .Case<int>([]() {})     // OK: first call works
            .Case<float>([]() {});  // REQUIRED: template keyword needed
    };
}

[](auto value) { ... } 内部是模板语境,Clang 把模板语境的 lambda(第一个 []() {})当成 dependent expression,导致 Switch<int>(value).Case<int>([]() {}) 这个表达式也变成 dependent expression。

删掉 .Case<int>([]() {}).Case<float>([]() {}) 都可以过编译。

不过 MSVC 能过编译。

9. 指针 == 在菱形继承下不能判断是同一对象

一般来说,判断指针相同可以直接 ==,因为编译期基类地址对于子类的偏移是确定的(偏移不为 0 的情况是多继承、有虚函数类继承无虚函数类)。

cpp
#include <cstdio>

struct Base1 {
    int data0;
};
struct Base2 {
    int data1;
};
struct Derived : Base1, Base2 {};

int main() {
    Derived d;
    Derived* derived_ptr = &d;
    Base1* base1_ptr = &d;
    Base2* base2_ptr = &d;

    printf("%p %p %p\n", derived_ptr, base1_ptr, base2_ptr);  // 可能输出 0x7ffcdf034dcc 0x7ffcdf034dcc 0x7ffcdf034dd0
    printf("%d %d\n", derived_ptr == base1_ptr, derived_ptr == base2_ptr);  // 输出 1 1
}

但是菱形继承是不行的,因为子类包含了多个基类(类型相同但是地址不同),就会有歧义。

cpp
#include <cstdio>

struct Base0 {};
struct Base1 : Base0 {
    int data0;
};
struct Base2 : Base0 {
    int data1;
};
struct Derived : Base1, Base2 {};

int main() {
    Derived d;
    Derived* derived_ptr = &d;
    Base0* base0_ptr = static_cast<Base1*>(&d);

    printf("%d\n", derived_ptr == base0_ptr);  // error: 'Base0' is an ambiguous base of 'Derived'
}

10. 死代码需要清理吗

例如:

cpp
if (condition) {
    return res;
    break;  // 永远不会执行
}

要清理。没啥大毛病,只是 error-prone,就是反向推测你可能想实现的是别的逻辑,但是写错了。

11. std::string::c_str() 的值移动后失效

cpp
#include <cstdio>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> a{"123"};
    auto p = a[0].c_str();
    printf("%s\n", a[0].c_str());  // 输出 123
    a.push_back("456");
    printf("%s\n", a[0].c_str());  // 输出 123
    printf("%s\n", p);             // 可能没有输出(GCC 15.1 不加编译参数)
    return 0;
}

SSO(小字符串优化)导致的。

按照标准的话,用来移动构造其他对象的 string,所有迭代器和指针都失效了。所以即使不满足 SSO 也不要这么用。

由此看出,用 vector<string> 当作 pool 容易触发这个问题,所以更好的实践是整个 std::hive 类似的对象池。

与之不同的是,std::vector::data() 的值移动后不会失效,https://zh.cppreference.com/w/cpp/container/vector/vector.html 的 Notes 章节。

12. 为什么没有 is_specialization 的模板工具

提案 https://wg21.link/P2098R0 被搁置了,原因是不够通用,没法完美地处理类型参数 + 非类型参数的排列组合。

例如,找不到这么一个 is_specialization_of 的定义:

cpp
template <typename T, template </* 这里填啥能同时满足下面的要求 */> typename Template> is_specialization_of {};
using T1 = is_specialization_of<std::array<int, 1>, std::array>;
using T2 = is_specialization_of<std::vector<int>, std::vector>;

往期提到的 universal template 提案可以处理这个问题,但是也没过。

C++26 反射能解决,但是模板参数类型得是反射得到的类型(std::meta::info),不能直接填类型和模板。

13. 群友的名言警句

保持对最佳实践的敏感真的很重要。问题可能千奇百怪,但如果团队平时就尽量去追求最佳实践,那很多都是可以避免的。—— bincat

做项目不是说会调几个库跑个 mvp 出来就行了,出现问题了你得会定位,某些情况下你得自己编下库把断点打到那里,你如果没有通用的相对 low level 的知识,效率会很低。—— hiki

14. 一些文章

Don't ask to ask, just ask https://dontasktoask.com/

jemalloc 生平,原文 https://jasone.github.io/2025/06/12/jemalloc-postmortem/ 翻译 https://zclll.com/index.php/default/jemalloc-postmortem.html

性能优化的一个完整过程 https://www.zhihu.com/question/486847589/answer/1925001370940990828

大脑根本扛不住“频繁决策”:为什么聪明人都在练习“决策节约”?https://mp.weixin.qq.com/s/32cXjx6Enn9OKUnmJqhcfg

A Programmer's Reading List https://www.piglei.com/articles/en-programmer-reading-list-part-one/

洞悉C++函数重载决议 https://zhuanlan.zhihu.com/p/561977606


都看到这了,来关注一下败犬日报吧!

主站 | 知乎专栏 | 微信公众号 | RSS