目录

败犬のC++每月精选 第 6 期

img

本月的 C++ 话题速览!(2025-03)

# 1. C++ 跳出双重循环或跳出 if 的方法

没有很好的做法。

最合理的做法可能是 lambda 包一层,用 return 跳出。

用 goto 容易违反项目规范。

You suddenly visualize that I am looking over your shoulders and say to yourself: "Dijkstra would not have liked this"

另外,break label 的提案是有的 https://open-std.org/JTC1/SC22/WG21/docs/papers/2025/p3568r0.html (opens new window)

# 2. 异常会导致二进制膨胀

写了异常会多一堆二进制代码来处理异常,一些项目禁止 RTTI 和异常都是这个理由。

例如 LLVM https://llvm.org/docs/CodingStandards.html#do-not-use-rtti-or-exceptions (opens new window)

Do not use RTTI or Exceptions

In an effort to reduce code and executable size, LLVM does not use exceptions or RTTI (runtime type information, for example, dynamic_cast<>).

That said, LLVM does make extensive use of a hand-rolled form of RTTI that use templates like isa<>, cast<>, and dyn_cast<>. This form of RTTI is opt-in and can be added to any class.

当然允许异常的项目同样也有很多,异常更简单省事。

不用异常可以用 std::expected 或类似的设计替代。


一个 cppcon 讲了如果返回错误码的代码过多,关闭 rtti 使用异常替换同样的逻辑可以做到更小的二进制体积。https://www.bilibili.com/video/BV14YyfYhE3C (opens new window) 第一集

# 3. 静态链接符号未找到,但目标文件有这个符号

是链接顺序的问题。在往期提到这个现象:

主程序没引用静态库里面的符号,可能导致静态库整个被丢弃,里面的全局变量初始化的副作用也会没

可以用 --whole-archive 来临时规避,但是会导致二进制膨胀问题。

最佳实践是 --start-group --end-group

https://stackoverflow.com/questions/52435705/whats-difference-between-start-group-and-whole-archive-in-ld (opens new window)

# 4. C 的资源释放问题

函数退出前要把之前的 malloc 全都 free 了,如果函数复杂就会有大量重复 free。

一般要用 goto 来解决,例如:

void func() {
    int *a, *b, *c;
    a = (int *)malloc(sizeof(int));
    if (a == NULL) {
        goto a_fail;
    }
    b = (int *)malloc(sizeof(int));
    if (b == NULL) {
        goto b_fail;
    }
    c = (int *)malloc(sizeof(int));
    if (c == NULL) {
        goto c_fail;
    }
    free(c);
c_fail:
    free(b);
b_fail:
    free(a);
a_fail:
}

如果项目禁止了 goto,可以用 attribute cleanup,类似 C++ 的 RAII。

# 5. unordered_map<string, int> 不能查找 string_view

下面的代码会报错 error: no matching function for call to 'xxx::find(std::string_view)'

#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> a;
    a.find(std::string_view());
}

虽然 https://zh.cppreference.com/w/cpp/container/unordered_map/find (opens new window) 在 C++20 之后支持了模板接口 template< class K > const_iterator find( const K& x ) const;,但是 Hash 和 KeyEqual 必须是透明的,所以需要自定义 Hash:

#include <iostream>
#include <string>
#include <unordered_map>

struct my_hash : std::hash<std::string>, std::hash<std::string_view> {
    using std::hash<std::string>::operator();
    using std::hash<std::string_view>::operator();
    using is_transparent = void;
};

int main() {
    std::unordered_map<std::string, int, my_hash, std::equal_to<>> a;
    a.find(std::string_view());
}

# 6. 如何用可变参数给数组赋值

构造函数比较简单。赋值麻烦一点,要用 integer_sequence。

#include <cstdlib>
#include <utility>

template <size_t N>
struct Array {
    int array[N];

    template <typename... Ts>
    Array(Ts... args) : array{std::move(args)...} {}

    template <typename... Ts>
    void assign(Ts... args) {
        [&]<size_t... Is>(std::index_sequence<Is...>) {
            (..., (array[Is] = std::move(args)));
        }(std::index_sequence_for<Ts...>{});
    }
};

# 7. 引用的非空 hint

引用不能为空,编译器会根据这点对代码进行优化。

https://zhuanlan.zhihu.com/p/665536071 (opens new window) 这个就讲了成员函数的 this 如果是 nullptr 会出问题的例子(即使成员函数没有使用 this)。

类似的还有 memcpy 也会有非空的假设 https://zhuanlan.zhihu.com/p/699259091 (opens new window)

# 8. 三路运算符 <=> 和强序弱序偏序

这些概念把比较运算给规范化了,区分了三种类型:

  1. std::strong_ordering “严格”地比较大小。
  2. std::weak_ordering 不“严格”地比较大小,比如按字典序比,但是忽略大小写;或者只看一部分属性,忽略另一部分属性。
  3. std::partial_ordering 可能有些东西没法比大小,比如 nan。

三路运算符解决了什么问题?

定义了 <=>< <= > >= 这 4 个运算符就能用了,写起来简单。


为什么定义了 <=> 还要定义 ==

性能考虑。例如字符串比较,== 只要长度不同就可以直接返回 false,而 <=> 还要区分大于小于。


如果 <=>== 冲突了会怎么办?

不影响,因为 < <= > >=<=> 引出,!=== 引出,两者没有交集,只是行为会很奇怪。

但是使用了 std::sort 之类的标准库函数是未定义行为。


推荐阅读

  1. https://hackingcpp.com/cpp/lang/comparisons.html (opens new window)
  2. https://zhuanlan.zhihu.com/p/350867708 (opens new window)
  3. https://github.com/xiaoweiChen/CXX20-The-Complete-Guide (opens new window) 第一章

# 9. 重载决议的一个例子

#include <cstdio>

template <typename T>
void f(T &&x) {
    printf("1\n");
}

void f(const int &x) { printf("2\n"); }

int main() {
    int x = 1;
    f(x);
}

答案是 1,const int &x 不是完美匹配(多了个 const)。

那么,在实践上如果有多个行为不一致的函数,怎么匹配想要的函数?

可以加 requires(但不一定是好的实践)。标准库的做法是换函数名,无法换函数名(比如构造函数),在函数形参开头加一个不同的占位参数(比如 std::optional 构造函数的 std::in_place_tstd::set 构造函数的 std::from_range_t)。

# 10. deducing this + CRTP 的隐藏坑

给某个类注入一个函数,然后这个类被别人继承,再通过派生类调用注入的函数,这时 self 是别人派生的那个类。

#include <iostream>

template <typename Derived>
struct Base {
    void foo(this auto&& self) {
        std::cout << "self is " << typeid(decltype(self)).name() << "\n";
    }
};

struct Derived : Base<Derived> {};

struct FurtherDerived : Derived {};

int main() {
    FurtherDerived fd;
    fd.foo();
}

输出是 self is 14FurtherDerived,self 推导成了 FurtherDerived 而不是期望的模板参数 Derived。

解决方法是直接不用 deducing this。(也可以给被注入的类加 final,但是不合理,因为增加了约定)

# 11. vector::push_back 如果有扩容,为什么先构造传入的元素而非原有元素

#include <iostream>
#include <vector>

struct A {
    int x;
    A(int x) : x(x) {}
    A(A&& other) noexcept {
        x = other.x;
        std::cout << "move " << x << "\n";
    }
};

int main() {
    std::vector<A> vec1;
    vec1.push_back(A(1));
    std::cout << "***\n";
    vec1.push_back(A(2));
    return 0;
}

输出:

move 1
***
move 2
move 1

标准没有规定顺序,实现上的顺序是为了满足“有无扩容表现一致”的要求。

例如 vec.push_back(vec.front()),如果先移动原有元素,vec.front() 就悬垂引用了。

再例如 vec.push_back(std::move(vec.front())),想想怎么实现 push_back 让有扩容和无扩容的表现一致?