我希望的 c++26

我希望的 c++26

本文使用 macbook pro m4 + vi 编写, 我使用的编译器是 g++-15 + glibc++.

本文讲了我希望的 c++26.

所有 c++23 代码都能通过编译

在目前的 iso c++26 下, c++23 允许的一些危险操作, 需要加上 [[unsafe]].

我支持的 iso c++26 提案特性

线性代数

这个不管设计成什么样我都能接受, 因为打 icpc 有用.

加强了 constexprconsteval

非常好的 constexpr, 使我的常量在编译期完成.

std::expected

这个值得重点讲, 非常好的错误处理.

比如说我有一个带取模的快速幂函数, 我希望模数非 \(0\), 就可以这么写:

cpp 复制代码
std::expected<long, std::string> pow(long x, long y, long mod) {
	if (mod == 0)
		return std::unexpected<std::string>("数学错误"s);

	long yt = 1;
	x %= mod;

	while (y) {
		if (y & 1)
			yt = yt * x % mod;

		x = x * x % mod;
		y >>= 1;
	}

	return yt;
}

然后就可以这样处理结果:

cpp 复制代码
auto ans = pow(x, y, m);

if (ans)
	std::println("{}", *ans);
else
	std::println("{}", ans.error());

我个人习惯使用 std::abort() 作为错误处理, 不过这样好像有点合理?

std::flat_setstd::flat_map

std::vector 存储的排序数组, 可以当成平衡树用, 适合查询多而插入删除修改少的情况.

我当然还是手写 wblt 的了.

std::is_within_lifetime

可以检测对象是否还活着, 遥遥领先了属于是.

对像我这样不手动操内存的人来说应该没啥用.

我非常反对的 iso c++26 提案特性

如果所有语言都在使劲浑身解数想要变成 rust, 那设计这个语言本来的目的是什么呢?

java 是为了替代 c++, 而 java 发明了一次编译到处运行的 jvm.

go 是为了替代 c++, 而 go 有通讯和可以比肩 python 的标准库.

rust 是为了替代 c++, 而 rust 搞出的是借用和可变借用.

我从来没说过 rust 不好, 我承认我用 rust 参与过洛谷的开发, 当然也用 rust 写过 opengl.

java 没有被 rust 同质化, java 依然到处都是空引用.

go 也没有被 rust 同质化, go 依然在用 java 发明的垃圾回收.

如果 c++ 变成了用 c 语法写的 rust, 那么 c++ 还有什么存在的意义吗? 难道是为了让 c 程序员过渡到 rust 中间有个缓冲吗?

我不是说 k&r 的设计优秀, 但我觉得作为包括了 java 和 go 甚至 c# 和 python 在内的 c like 语言, 危险当然是有危险的价值的. 可以是速度, 可以是自由度, 可以是任何东西, 重要的是危险本身可以带来价值.

危险指针

从提案设计来看, 很显然是 son of graydon.

[[unsafe]]

无论无论如何, 你认为的危险操作可能是我需要的.

而且这会导致使用了这些危险操作的 c++23 代码无法通过 c++26 的编译, 这违背了 c++ 的 较新的 c++ 标准应尽量兼容较旧标准的合法代码 原则.

另外, [[unsafe]] 的含义是 "除了这里之外都是安全的", 但实际上 c++ 语言本身应该全是 unsafe, 所以应该加 [[safe]] 来表示 "除了这里之外都是危险的", 所以 [[unsafe]] 是 graydon 的政变.

我想要额外加入的

contracts

本来是在 c++26 提案里的, 听说即将被删除了, 希望能加入吧.

使用编译指令 -fcontract-semantic=quick_enforce 可以让代码 fail fast.

fail fast: 见 lil_tea c++ style guide.

vla

vla 是一个优秀的设计, 就像 std::vector 一样, 但你可以用使用数组的方式来使用, 比如使用 std::sort(a, a + n) 而不需要写 std::sort(a.begin, a.end()).

有人说会炸空间, 正好我想放在堆区, 那就放在堆区. 但语法和 c99 的 vla 完全相同, 并且可以选择性调用默认构造函数.

你应该相信 new 的速度是非常非常快的, 不过我从来不相信我会手动释放, 所以我都是用智能指针或 std::vector.

而且堆区还有一个好处, 如果你用 g++ 编译代码, 你可以直接让 sizeof 返回 a[-1]. 注意这个 a[-1] 是 ub, 在生产代码中千万千万不要手写, 交给编译期来处理.

编译器记得负责释放, 别到时候没人释放就内存泄漏了.

注意: 堆上 vla 不属于 gc. 事实上我非常反对 c++ 加入 gc. 但是堆上 vla 的释放只是一个普通的数组离开作用域自动删除而已, 不属于 gc.

restrict

restrict 是个优秀的设计, 你向编译器承诺 我发誓我不会传入两个相同的东西, 这样编译器就可以放心的优化了.

我要说的是, 我想要 restrict 支持任何的除了传值之外的传参方式.

比如说, 我这样写:

cpp 复制代码
void	mul(matrix &s, const matrix &x, const matrix &y) {
	// 假装我在这里实现了矩阵乘法
}

那么如果 sx 是同一个对象 (很正常的 \(s *= y\) 语义), 那么这里是会导致错误的 (x 一边被 s 修改, 一边还要用乘积给 s 赋值, 简直就是胡闹).

所以我加上这个:

cpp 复制代码
void	mul(matrix &restrict s, const matrix &restrict x, const matrix &restrict y) {
	// 假装我在这里实现了矩阵乘法
}

然后让编译器帮我检查, 如果对象重叠就直接报错.

同样的, 也可以支持 stl 和迭代器, 也可以支持智能指针, 只要重叠就让编译器报错.

当然了, 这很明显是违背了最初的含义是用于优化代码, 但这确实可以让代码更加安全, 优化与否我确实不在意.

int main(const std::vector<std::string> &args)

这个的含义是非常显然的, 就是 java 学来的 public static void main(String[] args), 用了 c++ 写法.

在继续允许 int main(int argv, char **argv)int main(void) 的情况下, 再有一个现代化的版本也是非常优秀的.

注意: java 的 args[0] 已经是参数, 而我们根据 c 惯例让 args[0] 表示工作目录, 后面的 args[1]args[args.size() - 1] 才是参数.

超级基类

有一个类, 我不想让它被实例化, 也不想让它被用于传参, 但有很多类都要继承它, 那么我就把这个类写成超级基类. 超级基类可以有虚函数, 可以有纯虚函数, 也可以没有虚函数, 可以继承一个超级基类, 只要不实例化且不做参数就行.

cpp 复制代码
baseclass entity {
	std::vector<double> pos_;

public:
	entity(std::size_t d)
		: pos_(d) {}

	std::expected<void, std::string> move(const std::vector<double> &velocity, long time) {
		if (velocity.size() != pos_.size())
			std::unexpected<std::string>("纬度错误");
		if (time < 0)
			std::unexpected<std::string>("时间错误");

		for (std::size_t x : std::views::iota(0uz, pos_.size()))
			pos_[x] += velocity[x] * time;
	}

	const std::vector<double>& where(void) const {
		return pos_;
	}
};

class	player : entity;
class	zombie : entity;
baseclass npc : entity;
class	merchant : npc;

额外的, 如果你让一个超级基类继承一个普通类, 那我不会拦着你, 但你应该想清楚你要做什么再开始敲键盘.

说到键盘, 我刚在网上买了一个 ibm model m 的 \(87\) 键客制化机械键盘, 我放了 \(87\) 个青轴进去. 还是老键盘用着舒服.

函数认为参数

有一个递归函数, 我想让人在调用的时候传入一个我想要的初始化值. 如果用默认参数还是会很容易被传入错误的值, 这时候就需要语言层面来解决了.

我们让 wanted 表示期待这个值, 那么从函数外面调用这个函数则无论传入多少都会当成这个期待的值, 函数内部的递归调用不受影响可以随便写.

比如说这个用于标记深度的函数:

cpp 复制代码
void	vtx::dfs(long depth = wanted 1) {	// 根的深度一定是 $0$
	depth_ = depth;
	for (vtx *y : to_) {
		if (y->depth_)	// 搜到了先人, 不管
			continue;

		y->dfs(this, depth_ + 1);	// 搜到了后人
	}
}

那么调用的时候有几种方法:

cpp 复制代码
root->dfs();	// 直接用 $1$
root->dfs(999);	// $999$ 被忽略, 还是用 $1$
root->dfs(1);	// $1$ 被忽略, 还是用 $1$