三行情诗
谁说我们写 C++ 的就不懂浪漫了:
// 我的心必须通过"爱你"的约束才能实例化
template<typename Heart> requires loves<Heart, You>
// 激情、忠诚、永恒置入爱的元组,这份爱在编译时就已确定,不会因运行时的变化而改变
// 是写在源码里的承诺,是嵌入二进制的誓言
constexpr auto my_feelings = std::tuple{passion, devotion, eternity};
// 在 C++ 的类型系统中,我们的未来被特化为永远为真,这是编译器都认可的事实
template<> struct future<us> : std::true_type { static constexpr auto value = forever; };
不愧是我们写 C++ 的,写出来的情诗都能是零运行时开销的。
AND
下面这段代码能正常编译运行:
int and a = 1; // 是的你没有看错,这里有个 and...
a = 2; // 是的你还是没有看错,这个 and a 还能被赋值
这一段代码有两个非常有意思的地方,首先第一行的and
本身是一个非常冷门但是古老的关键字,ISO 646 标准引入的——与符号(&&
)的别名,早期很多键盘无法输入特殊符号所以引入了这个关键字。
在很多编译环境中,and
被实现成了宏 #define and &&
,所以这段代码就变成了:int && a=1
,&&
这个符号还表示右值引用的意思,这里创建了一个对于右值常量 1 的右值引用。
第二行有意思的点可以用一句话描述:”右值引用其实是个左值
”,这里就不详细展开了。
CRTP
CRTP(Curiously Recurring Template Pattern),谈奇怪的代码肯定离不开著名的“奇异递归模板模式”,这个技巧在网上被宣传的很高大上:“静态多态”,“vtable killer”,“零开销抽象神迹的体现”,但是实际上我们 99% 场景下的多态是需要将各个实例放在一个容器中的,为了优化虚函数表查找开销,我们也可以使用传统递归代码直接使用子类。
CRTP 更大的意义在于父类(基类模板)可以直接“感知”并利用子类(派生类)的类型信息和具体实现。这使得它能够优雅地实现Mixin
功能,为派生类提供通用行为或工具,而不仅仅是传统的“is-a”意义上的继承
。
template <typename T> struct Base {
// 这里 T 会传入子类的类型, 父类拿到了子类的类型,这就是递归
// 直接强转成子类指针调用,就没有虚函数表查找开销
// Derived1 Derived2 Derived3 在被父类调用时都能无损调用到 do_something_impl
void do_something() { static_cast<T *>(this)->do_something_impl(); }
};
struct Derived : Base<Derived> {
void do_something_impl() { std::cout << "Derived impl\n"; }
};
自毁对象
这个类会在构造的一瞬间就离开我们:
struct Kamikaze {
Kamikaze() { std::cout << "我出生了!\n"; delete this; }
~Kamikaze() { std::cout << "我死了!\n"; }
};
Kamikaze* k = new Kamikaze(); // 输出 “我出生了!” 和 “我死了!”
使用了
delete this
语法在对象的内部触发删除自身,在某些情况下可以让对象自己掌控自己的生命周期(我的命运不受他人摆布!),比如事件驱动系统中的自清理监听器、插件系统自我卸载等等。
无敌的对象
C++ 的对象拥有彻底掌控自己命运的能力,既可以选择什么时候死亡,也可以选择拒绝死亡。
#include <iostream>
#include <new>
class Phoenix {
public:
int i = 42;
void operator delete(void *) {} // 重写 delete,避免内存被释放,这样就只会执行析构函数但是不会释放内存
~Phoenix() { std::cout << "休想杀死我,我是无敌的!!" << std::endl; }
};
int main() {
Phoenix *p = new Phoenix();
std::cout << "p->i = " << p->i << std::endl;
delete p;
delete p;
delete p;
delete p;
delete p;
std::cout << "p->i = " << p->i << std::endl; // 对象仍然可以正常使用
}
The Coolest C++ Code
一千个人心中有一千种最酷的 C++
代码,但是最 Cool 的代码莫过于此:
#include <iostream>
using namespace std;
int main()
{
cout << 0 << "K" << endl; // 输出:0K
}
0K = -273.15℃,绝对零度(Absolute Zero),very cool
一段非常简单的标准库代码
C++ 的标准库是我目前见到的所有编程语言中最复杂的没有之一,且远胜其他编程语言,下面列举一小段 std::vector
(动态数组)插入函数的部分相关代码,动态数组相对其他数据结构来说已经是非常简单的了,但是其复杂度仍然相当之高,标准库中充斥着各种多层模板嵌套、历史分支兼容与宏定义,我很难想象一名刚刚学完谭浩强 C++ 的新人不小心点到标准库源代码文件会是什么反应:
template <class _Tp, class _Allocator>
template <class _Up>
_LIBCPP_CONSTEXPR_SINCE_CXX20 typename vector<_Tp, _Allocator>::pointer
vector<_Tp, _Allocator>::__push_back_slow_path(_Up&& __x) {
allocator_type& __a = this->__alloc();
__split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), size(), __a);
// __v.push_back(std::forward<_Up>(__x));
__alloc_traits::construct(__a, std::__to_address(__v.__end_), std::forward<_Up>(__x));
__v.__end_++;
__swap_out_circular_buffer(__v);
return this->__end_;
}
template <class _Tp, class _Allocator>
_LIBCPP_CONSTEXPR_SINCE_CXX20 inline _LIBCPP_HIDE_FROM_ABI void
vector<_Tp, _Allocator>::push_back(const_reference __x) {
pointer __end = this->__end_;
if (__end < this->__end_cap()) {
__construct_one_at_end(__x);
++__end;
} else {
__end = __push_back_slow_path(__x);
}
this->__end_ = __end;
}
template <class _Tp, class... _Args, class = __enable_if_t<__has_construct<allocator_type, _Tp*, _Args...>::value> >
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 static void
construct(allocator_type& __a, _Tp* __p, _Args&&... __args) {
_LIBCPP_SUPPRESS_DEPRECATED_PUSH
__a.construct(__p, std::forward<_Args>(__args)...);
_LIBCPP_SUPPRESS_DEPRECATED_POP
}
template <class _Tp,
class... _Args,
class = void,
class = __enable_if_t<!__has_construct<allocator_type, _Tp*, _Args...>::value> >
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 static void
construct(allocator_type&, _Tp* __p, _Args&&... __args) {
std::__construct_at(__p, std::forward<_Args>(__args)...);
}
太空船运算符
太空船运算符并不是什么用户自创的梗,是实打实的内置语法(since C++ 20),官方名字是(Three-way Comparison Operator)但是真的很神奇!
在没有太空船之间,我们要实现对象完整的比较运算符需要写这么大一堆代码:
struct DateTime {
int year, month, day, hour, minute, second;
bool operator<(const DateTime& rhs) const {
if (year != rhs.year) return year < rhs.year;
if (month != rhs.month) return month < rhs.month;
if (day != rhs.day) return day < rhs.day;
if (hour != rhs.hour) return hour < rhs.hour;
if (minute != rhs.minute) return minute < rhs.minute;
return second < rhs.second;
}
bool operator==(const DateTime& rhs) const {
return year == rhs.year &&
month == rhs.month &&
day == rhs.day &&
hour == rhs.hour &&
minute == rhs.minute &&
second == rhs.second;
}
bool operator!=(const DateTime& rhs) const { return !(*this == rhs); }
bool operator>(const DateTime& rhs) const { return rhs < *this; }
bool operator<=(const DateTime& rhs) const { return !(rhs < *this); }
bool operator>=(const DateTime& rhs) const { return !(*this < rhs); }
};
尽管后面可以用 std::tie
或者 std::rel_ops
等各种手段简化,这仍然是一件非常 boring 的事情。
有了太空船以后,这些代码就都能自动生成了:
struct DateTime {
int year, month, day, hour, minute, second;
auto operator<=>(const DateTime &) const = default;
};
默认是一个一个按字段自然排序,太空船也是支持自定义比较顺序的:
struct Score {
int chinese;
int english;
int math;
auto operator<=>(const Score &rhs) const {
auto lhs_total = chinese + english + math;
auto rhs_total = rhs.chinese + rhs.english + rhs.math;
return lhs_total <=> rhs_total; // 将两个结构体的太空船,转换为基本类型的太空船
}
};
伪随机数
在 C++ 中,如果想要或者一个随机数(伪),除了 std::rand
之外,这也是一种方式 :)
会输出什么全取决于内存中残留着什么——也就是不可预知的随机行为。
#include <iostream>
int main() {
int64_t i;
std::cout << i++;
}
定义了变量 i 之后只会申请 8 个字节的内存,需要手动赋值才会进行初始化,所以不存在一个默认 0 初始化值。
尽管这很可能非常反直觉,很多其他语言的用户会觉得踩坑了。
但这正是 C++ 核心设计哲学:You don't pay for what you don't use
的体现,在操作系统和硬件层面,申请内存和初始化本来就是两个分开的操作,你只申请了内存,那 C++ 也不会画蛇添足。
万物皆可重载
C++ 里面所有你能想到的运算符都是可以重载的,包括 new delete 甚至是后缀和逗号,至于为什么要重载这么多符号,因为不希望我的类别人拿起来就能用 :)
#include <iostream>
#include <new>
class Everything {
public:
explicit Everything(int val = 0) : value(val) { log("Constructor"); }
// 一元运算符重载
Everything operator+() const { log("+ (unary)"); return *this; }
Everything operator-() const { log("- (unary)"); return Everything(-value); }
Everything operator~() const { log("~"); return Everything(~value); }
bool operator!() const { log("!"); return !value; }
Everything& operator++() { log("++ (prefix)"); ++value; return *this; }
Everything operator++(int) { log("++ (postfix)"); Everything temp = *this; ++value; return temp; }
Everything& operator--() { log("-- (prefix)"); --value; return *this; }
Everything operator--(int) { log("-- (postfix)"); Everything temp = *this; --value; return temp; }
// 二元运算符重载
Everything operator+(const Everything& other) const { log("+ (binary)"); return Everything(value + other.value); }
Everything operator-(const Everything& other) const { log("- (binary)"); return Everything(value - other.value); }
Everything operator*(const Everything& other) const { log("*"); return Everything(value * other.value); }
Everything operator/(const Everything& other) const { log("/"); return Everything(value / other.value); }
Everything operator%(const Everything& other) const { log("%"); return Everything(value % other.value); }
Everything operator&(const Everything& other) const { log("& (binary)"); return Everything(value & other.value); }
Everything operator|(const Everything& other) const { log("|"); return Everything(value | other.value); }
Everything operator^(const Everything& other) const { log("^"); return Everything(value ^ other.value); }
Everything operator<<(const Everything& shift) const { log("<<"); return Everything(value << shift.value); }
Everything operator>>(const Everything& shift) const { log(">>"); return Everything(value >> shift.value); }
bool operator==(const Everything& other) const { log("=="); return value == other.value; }
bool operator!=(const Everything& other) const { log("!="); return value != other.value; }
bool operator<(const Everything& other) const { log("<"); return value < other.value; }
bool operator>(const Everything& other) const { log(">"); return value > other.value; }
bool operator<=(const Everything& other) const { log("<="); return value <= other.value; }
bool operator>=(const Everything& other) const { log(">="); return value >= other.value; }
bool operator&&(const Everything& other) const { log("&&"); return value && other.value; }
bool operator||(const Everything& other) const { log("||"); return value || other.value; }
// 赋值运算符重载
Everything& operator=(const Everything& other) { log("="); value = other.value; return *this; }
Everything& operator+=(const Everything& other) { log("+="); value += other.value; return *this; }
Everything& operator-=(const Everything& other) { log("-="); value -= other.value; return *this; }
Everything& operator*=(const Everything& other) { log("*="); value *= other.value; return *this; }
// 特殊运算符重载
Everything operator,(const Everything& other) const { log(","); return other; }
Everything& operator[](const Everything& index) { log("[]"); value = index.value; return *this; }
Everything* operator->() { log("->"); return this; }
Everything operator->*(Everything* ptr) { log("->*"); return *ptr; }
int operator()(const Everything& arg) { log("()"); return value + arg.value; }
// new/delete 重载
static void* operator new(size_t size) { log("new"); return ::new char[size]; }
static void operator delete(void* ptr) { log("delete"); ::delete[] static_cast<char*>(ptr); }
// 隐式转换到int
operator int() const { log("implicit conversion to int"); return value; }
int getValue() const { return value; }
static void log(const char* op) { std::cout << "Overloaded " << op << "!\n"; }
private:
int value;
};
// UDL重载(扩展多个后缀:_ms, _s, _us)
Everything operator"" _ms(unsigned long long val) {
Everything::log("literal _ms");
return Everything(static_cast<int>(val));
}
Everything operator"" _s(unsigned long long val) {
Everything::log("literal _s");
return Everything(static_cast<int>(val * 1000));
}
Everything operator"" _us(unsigned long long val) {
Everything::log("literal _us");
return Everything(static_cast<int>(val / 1000));
}
int main() {
Everything e(42);
auto result = (new Everything((((((e + 42_ms) * e) / e % e & e | e ^ e) << 1_s) >> 1_ms, ((e == e) != (e < e) > (e <= e) >= (e && e) || e), (e += e -= e *= e))[0_us]->operator()(1_ms) + e++ -- + - ~ ! e));
std::cout << "Final result: " << result << std::endl;
return 0;
}
数组下标运算符加法交换律
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 等等...这个 1[arr] 是什么写法?
std::cout << "The value is: " << 1[arr] << std::endl;
// 输出:The value is 20
}
这个是因为数组下标运算符 []
方括号在底层的实现方式是:a[b] => *(a + b)
,由于加法运算具有交换性,*(b + a)
b 在前面还是后面都是等价的,所以 1[arr] => *(1 + arr) = *(arr + 1) = arr[1]
。
扩展一下,对于二阶数组来说,这两种写法也是等价的:
arr[2][1] = 1[2[arr]]
int num = 0x123'full; // num == 0x123 * 16 + 15
解析:’ 是个无意义的分隔符,完整的数字是:123f 十六进制,后面的 ull 表示数字格式为 unsigned long long,所以加上这个关键字就是在数字末尾加了个 f,进一位再加 15。
Duff’s Device
Duff's Device
是一段非常经典的 C 优化代码,这段代码的作者是 Tom Duff
,当他在卢卡斯影业(制作星球大战的公司)工作时,为了榨干一台性能有限的图形终端的全部机能而发明了此方法。
假设我们有一段数据 from
要 复制到to
里面,我们是不是很自然的会写出来如下代码:
void send(int* to, int* from, int count) {
for (int i = 0; i < count; ++i) {
to[i] = from[i];
}
}
在几十年前的硬件和编译器环境下,上面这两种写法的循环开销 (loop overhead) 是一个不容忽视的问题。对于每一次拷贝,CPU 都需要执行:
- 一次条件判断 (
i < count
或count > 0
) - 一次或多次变量递增/递减 (
++i
或count--
) - 一次跳转指令 (跳回循环的开始)
Tom Duff
巧妙的利用了 switch 穿透的特性,实现了一个非常高性能的复制版本,在这个版本中,条件判断与跳转指令开销减少到了 1⁄8 (之所以是 8 是因为整数计算更快),整体时间复杂度从 O(n) 变为了 O(n/8),堪称最早的手动循环展开(Loop Unrolling):
void duffs_device_send(int* to, int* from, int count) {
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
当然在 21 世纪的今天,我们已经不需要再去写这种代码了,编译器会帮我们自动完成这一部分工作。
基于 switch 的协程
C++ 和 Python 这些语言的协程并不像 Go 的协程那样高级,本质上都只是一个“可暂停和恢复的函数”
,搭配上一些关键字用以插入状态转移点,保存一下栈帧。
所以理论上,我们可以基于 switch + __LINE__
实现一个简单的协程,每次调用 COROUTINE_YIELD
生成一个状态转移点,C++ 20 的协程本质上也是这种思路。
#include <iostream>
#include <string>
#define COROUTINE_START switch(state){case 0:
#define COROUTINE_YIELD(x) do{state=__LINE__;return x;case __LINE__:;}while(0)
#define COROUTINE_END }
class ProgressCoroutine {
public:
ProgressCoroutine(const std::string &name) : name_(name), state(0) {}
bool Run() {
COROUTINE_START;
std::cout << "[" << name_ << "] PROGRESS:25%" << std::endl;
COROUTINE_YIELD(true);
std::cout << "[" << name_ << "] PROGRESS:50%" << std::endl;
COROUTINE_YIELD(true);
std::cout << "[" << name_ << "] PROGRESS:100%" << std::endl;
COROUTINE_END;
return false;
}
private:
std::string name_;
int state;
};
int main() {
ProgressCoroutine foo("FOO");
ProgressCoroutine bar("BAR");
bool foo_running = true, bar_running = true;
while (foo_running || bar_running) {
if (foo_running)
foo_running = foo.Run();
if (bar_running)
bar_running = bar.Run();
}
std::cout << "ALL FINISHED!" << std::endl;
return 0;
}
在这个 demo
里面 Run()
函数的执行被中断了多次才断断续续完成。