
败犬のC++每月精选 2026-01
1. 面试题:memmove 怎么实现
memmove 是允许部分重叠的 memcpy,只要判断源和目标指针的大小,然后选择正向拷贝或反向拷贝,代码如下:
void memmove(char* dst, const char* src, int n) {
if (dst < src) {
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
} else {
for (int i = n - 1; i != -1; i--) {
dst[i] = src[i];
}
}
}实测这两个简单的 for 循环,不论正向反向,都能编译器自动向量化,性能是没问题的。
往期讲过独立变量比较地址的结果未指定,这时候正向反向的效果都一样,所以 dst < src 是没问题的。
要改进代码可以判断 dst == src 就直接 return,如果完全不重合可以直接调用 memcpy。
2. 关于 container_of 的未定义行为
container_of 在 C 里有争议(但是广泛使用,不会出问题),但是 C++ 里是没有争议的 UB(但也是广泛使用)。
一般来说 container_of 定义是 #define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member))),UB 发生在指针减法(导致数组越界),往期聊过这个问题。
例如:
struct A { int x, y, z; };
void unknown(int*);
bool foo() {
A a{0, 1, 2};
unknown(&a.y);
return a.x == 0 && a.z == 2; // 可能优化为永真,当然事实上不会这么优化
}用 container_of 可以修改 x 字段的值,代码可以这么写:
#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member)))
void unknown(int*y) {
container_of(y, A, y)->x = 1;
}3. 左值隐式转换后匹配到右值引用
这是一道 cppquiz:
#include <iostream>
void f(float &&) { std::cout << "f"; }
void f(int &&) { std::cout << "i"; }
template <typename... T>
void g(T &&... v)
{
(f(v), ...);
}
int main()
{
g(1.0f, 2);
}答案是输出 if,非常反直觉。
f(1.0f), f(2) 从左到右求值没问题,关键在于 v 是具名的,是个左值,不能匹配到自己类型的右值引用,选择隐式转换成临时值绑定 float lvalue -> int prvalue int lvalue -> float prvalue。
4. libstdc++ unordered_map.clear() 复杂度是错的
是关于桶数线性复杂度,而不是关于元素个数。
因为 unordered_map 的桶是惰性释放的(类似 vector.clear() 不释放内存),如果有很多桶再多次 clear() 会有明显性能问题。
这是 10 年前的问题 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67922。人手不足的情况下,不重要的 bug 就是没时间处理的。别说 10 年,Clang 还有 15+ 年前提的 bug 没修。
没人手处理这些东西确实是个问题,刚刚有编译器小团体建议放慢标准吸纳新特性的速度 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3962r0.pdf
5. 不同 TU 的同名 lambda 全局变量会报链接错误吗
这其实取决于编译器是否生成了符号。下面这个代码,用 GCC 10+ 编译可以得到符号 f。
auto f = [] {};g++ test.cpp -c -o test.o && nm test.o - 0000000000000000 B f
Clang / GCC 9- 不生成符号,因为 lambda 只在内部使用,别的 TU 无法获得这个变量(这样的定义没办法写声明),就不需要生成符号。
由于 ODR 违背是 UB,那么有没有链接错误都是符合预期的。这里详细说明一下,链接属于实现层面,标准只定义了 ODR,违背这个规则就是 UB。标准不会规定具体符号怎么生成,链接怎么做。
我们换个思路让 Clang 生成符号,比如:
inline auto g = [] {};
// extern decltype(g) f;
auto f = g;虽然 f 还是 lambda 类型,但是 f 可以被声明成 extern decltype(g) f;,所以 Clang 不得不生成了符号 f。
6. 检查函数传参传错顺序
clang-tidy 有这个检查 https://clang.llvm.org/extra/clang-tidy/checks/bugprone/easily-swappable-parameters.html。
解决方法也很简单,把几个参数打包成一个结构体参数 xxxparams。
最近有一篇 C 语言提案 strong typedef,用来解决类似的问题。
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3320.htm
[[strong]] typedef int Width;
[[strong]] typedef int Height;
void draw_rect(Width w, Height h);
Width w = 3;
Height h = 4;
draw_rect(w, h);
// draw_rect(h, w);不过这个提案放到 C++ 的问题很多 https://discourse.llvm.org/t/rfc-clang-adding-strong-typedefs/88843,不用期待了。
7. 踩坑之 for (auto i = v.size() - 1; i >= 0; i--)
这个代码的问题是 auto 推导出来是 size_t,所以 i >= 0 永真,死循环了。好在编译参数 -Wextra 也能发现问题 warning: comparison of unsigned expression in ‘>= 0’ is always true [-Wtype-limits]。
看到 v.size() - 1 的这个减法就应该警惕了,如果 v.size() == 0,i 初值就是很大的数,容易出问题。
第二个问题就是 auto 滥用了,不能一眼看出类型,直接写 int64_t 也不会麻烦多少。
8. benchmark 怎么阻止优化
DoNotOptimize 肯定是最佳实践,可以抄 google benchmark 的代码:
template <class Tp>
inline BENCHMARK_ALWAYS_INLINE void DoNotOptimize(Tp&& value) {
#if defined(__clang__)
asm volatile("" : "+r,m"(value) : : "memory");
#else
asm volatile("" : "+m,r"(value) : : "memory");
#endif
}然后看汇编对不对,如果耗时大的函数分离 TU 也行。
9. 编译期检查类的一些字段非空
一般来说可以用构造函数传递所有字段,从而保证非空。群友给了 builder + 模板的办法,不知道实用性如何(代码量大,建议在代码生成时使用):
#include <iostream>
struct A {
int x;
std::string y;
};
template <bool is_set_x = false, bool is_set_y = false, bool is_set_z = false>
struct A_Builder {
static A_Builder<false, false, false> create() {
return A_Builder<false, false, false>();
}
A_Builder<true, is_set_y, is_set_z> set_x(int x) {
A_Builder<true, is_set_y, is_set_z> rhs;
rhs.a_ = a_;
rhs.a_.x = x;
return rhs;
}
A_Builder<is_set_x, true, is_set_z> set_y(const std::string& y) {
A_Builder<is_set_x, true, is_set_z> rhs;
rhs.a_ = a_;
rhs.a_.y = y;
return rhs;
}
A build() {
static_assert(is_set_x, "Field x is not set");
static_assert(is_set_y, "Field y is not set");
return a_;
}
private:
template <bool Sx, bool Sy, bool Sz>
friend struct A_Builder;
A a_;
};
int main() {
A a = A_Builder<>::create().set_x(42).set_y("Hello").build();
std::cout << "A.x: " << a.x << ", A.y: " << a.y << std::endl;
}也可以实现 required<T>,然后用聚合初始化:
#include <iostream>
template <typename T>
struct required {
T data;
template <typename U>
required(U&& data) : data(data) {}
};
struct A {
int x;
std::string y;
};
struct A_Builder {
required<int> x;
required<std::string> y;
A build() { return A{std::move(x.data), std::move(y.data)}; }
};
int main() {
A a = A_Builder{.x = 42, .y = "Hello"}.build();
std::cout << "A.x: " << a.x << ", A.y: " << a.y << std::endl;
return 0;
}10. 推荐阅读
大模型的幻觉问题调研: LLM Hallucination Survey
Bjarne Stroustrup:现代 C++ 的跨世纪演进(演讲全文),讲技术的部分有资源管理,基于概念的泛型编程,模块,核心指南。
How Terminals Work,带交互的博客,挺有意思。