Matt Godbolt 通过展示 C++ 让我爱上了 Rust
[正文内容]
Matt Godbolt sold me on Rust (by showing me C++)
Gustavo Noronha May 06, 2025
阅读时间:8 分钟
因为 Compiler Explorer 而闻名的 Matt Godbolt 非常棒,你应该在网上搜索他发布的每一条内容。我当时就是这么做的,然后我观看了 Correct by Construction: APIs That Are Easy to Use and Hard to Misuse。 在从事 C/C++ 开发 20 多年后,这个主题引起了我的强烈共鸣。
在观看演讲时,我一直在想“没错!这就是为什么 Rust 会这样做。”看完后,我认为这个演讲实际上是让你直观地了解 Rust 如何在你所理解的内存安全之外提供帮助的一个好方法,而这篇文章的目的就是展示这一点。
但在讨论这个问题之前,我们应该先讨论 Matt 提出的问题以及他如何在 C++ 中解决这些问题。帮自己一个忙,观看完整的演讲,但让我分解其中的一个问题!
类型里有什么
Matt 在演讲开始时展示了一个将订单发送到证券交易所的函数可能是什么样子。
void sendOrder(const char *symbol, bool buy, int quantity, double price)
在深入之前,我想说他承认浮点数不适合 price
,并且稍后会谈到他通常如何处理它。但这构成了一个很好的例子,请耐心听我说。
另一个任何习惯于此的人都会立即指出的明显改进是用于识别购买的 bool
值。 这很容易出错,Matt 在本节结尾也提到了这一点。
但他首先关注的是 quantity
和 price
,以及 C++ 如何让你很难阻止调用者混淆它们:编译器将允许 quantity
为 1000.00
,price
为 100
,而不会发出任何警告,即使它们是不同的类型。它只是静默转换。
那么类型别名呢?
#include <iostream>
using Price = double;
using Quantity = int;
void sendOrder(const char *symbol, bool buy, int quantity, double price) {
std::cout << symbol << " " << buy << " " << quantity << " " << price
<< std::endl;
}
int main(void) {
sendOrder("GOOG", false, Quantity(100), Price(1000.00)); // Correct
sendOrder("GOOG", false, Price(1000.00), Quantity(100)); // Wrong
}
没有用! clang 19 和 gcc 14 都会接受它并且不会抱怨 —— 甚至使用了 -std=c++23 -Wall -Wextra -Wpedantic
,这是我在本文中用于所有 C++ 代码的选项! 经过几轮改进,我们得到了以下版本:
#include <iostream>
class Price {
public:
explicit Price(double price) : m_price(price) {};
double m_price;
};
class Quantity {
public:
explicit Quantity(unsigned int quantity) : m_quantity(quantity) {};
unsigned int m_quantity;
};
void sendOrder(const char *symbol, bool buy, Quantity quantity, Price price) {
std::cout << symbol << " " << buy << " " << quantity.m_quantity << " "
<< price.m_price << std::endl;
}
int main(void) {
sendOrder("GOOG", false, Quantity(100), Price(1000.00)); // Correct
sendOrder("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong
}
我们有类,我们有显式构造函数(非常重要,否则 C++ 会让你栽跟头!),我们有无符号类型……现在很难把 Price
放在你需要 Quantity
的地方!但是我们仍然可以给 Quantity
一个负值,而没有一个编译器警告,即使我们改用了无符号类型。再多一点魔法,我们就可以让它消失:
#include <iostream>
#include <type_traits>
class Price {
public:
explicit Price(double price) : m_price(price) {};
double m_price;
};
class Quantity {
public:
template <typename T> explicit Quantity(T quantity) : m_quantity(quantity) {
static_assert(std::is_unsigned<T>::value, "Please use only unsigned types");
}
unsigned int m_quantity;
};
void sendOrder(const char *symbol, bool buy, Quantity quantity, Price price) {
std::cout << symbol << " " << buy << " " << quantity.m_quantity << " "
<< price.m_price << std::endl;
}
int main(void) {
sendOrder("GOOG", false, Quantity(100u), Price(1000.00)); // Correct
sendOrder("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong
}
最后,我们可以让 clang(和 gcc)大声抱怨这种滥用。这一切只需要一个模板构造函数,它在编译时执行静态断言。好极了!
order/order-5.cpp:13:19: error: static assertion failed due to requirement 'std::is_unsigned<T>::value': Please use only unsigned types
13 | static_assert(std::is_unsigned<T>::value, "Please use only unsigned types");
| ^~~~~~~~~~~~~~~~~~~~~
order/order-5.cpp:26:28: note: in instantiation of function template specialization 'Quantity<int>::Quantity<int>(int)' requested here
26 | sendOrder("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong
| ^
1 error generated.
代码很多,但至少我们现在有了编译器保护。我们很棒了,没有其他方法可以滥用 quantity
和 price
了。对吗?
如果我们必须传入用户在 UI 上输入的值,因此我们需要从字符串转换它呢?那么,你又倒霉了:
sendOrder("GOOG", false, Quantity(static_cast<unsigned int>(atoi("-100"))),
Price(1000.00)); // Wrong
它不仅不会编译失败,而且也不会产生任何运行时错误。你最终会卖出 4294967196 股股票并破产。
不太理想。
Matt 继续展示了一些其他的技巧(以及它们的缺陷),以执行你需要的运行时检查来完全防御这种情况。我认为现在我们可以在 C++ 方面停下来,看看 Rust 是如何做的,好吗?
进入 Rust
那么 Rust 会更好吗? Rust 拥有数十年来从所有这些问题中学习的优势。并且确实学到了。让我们来看看。我们的第一次尝试表现如何?
fn send_order(symbol: &str, buy: bool, quantity: i64, price: f64) {
println!("{symbol} {buy} {quantity} {price}");
}
fn main() {
send_order("GOOG", false, 100, 1000.00); // Correct
send_order("GOOG", false, 1000.00, 100); // Wrong
}
在我们用 C++ 完成了所有工作之后,它不可能是这么容易,对吧?
error[E0308]: arguments to this function are incorrect
--> order/order-1.rs:7:5
|
7 | send_order("GOOG", false, 1000.00, 100); // Wrong
| ^^^^^^^^^^ ------- --- expected `f64`, found `{integer}`
| |
| expected `i64`, found `{float}`
|
note: function defined here
--> order/order-1.rs:1:4
|
1 | fn send_order(symbol: &str, buy: bool, quantity: i64, price: f64) {
| ^^^^^^^^^^ ------------ --------- ------------- ----------
help: swap these arguments
|
7 | send_order("GOOG", false, 100, 1000.00); // Wrong
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
好吧,你知道吗?它甚至告诉我们交换了哪些参数,以及所有内容。 就像我们生活在未来一样!好的,但是数字仍然很容易混淆,我们也可以像在 C++ 中那样创建类型,使其更加明确,对吧? 确实,让我们通过从 i64
切换到 u64
来防止负值。 这么容易吗?
struct Price(pub f64);
struct Quantity(pub u64);
fn send_order(symbol: &str, buy: bool, quantity: Quantity, price: Price) {
println!("{symbol} {buy} {} {}", quantity.0, price.0);
}
fn main() {
send_order("GOOG", false, Quantity(100), Price(1000.00)); // Correct
send_order("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong
}
是的,就是这么容易:
error[E0600]: cannot apply unary operator `-` to type `u64`
--> order/order-4.rs:10:40
|
10 | send_order("GOOG", false, Quantity(-100), Price(1000.00)); // Wrong
| ^^^^ cannot apply unary operator `-`
|
= note: unsigned values cannot be negated
好的,剩下的就是我们需要从用户输入的字符串中转换数字的情况。 我抓住你了,Rust……运行时输入不是你可以在编译时修复的东西,你如何才能改进 C++ 案例?
struct Price(pub f64);
struct Quantity(pub u64);
fn send_order(symbol: &str, buy: bool, quantity: Quantity, price: Price) {
println!("{symbol} {buy} {} {}", quantity.0, price.0);
}
fn main() {
send_order("GOOG", false, Quantity(100), Price(1000.00)); // Correct
send_order(
"GOOG",
false,
Quantity("-100".parse::<u64>().unwrap()),
Price(1000.00),
); // Wrong
}
强制用户处理由目标类型无法表示我们在字符串中的数字而导致的潜在错误转换,这有帮助吗?
error[E0308]: mismatched types
--> order/order-6.rs:13:18
|
13 | Quantity("-100".parse::<u64>()),
| -------- ^^^^^^^^^^^^^^^^^^^^^ expected `u64`, found `Result<u64, ParseIntError>`
| |
| arguments to this struct are incorrect
|
= note: expected type `u64`
found enum `Result<u64, ParseIntError>`
note: tuple struct defined here
--> order/order-6.rs:2:8
|
2 | struct Quantity(pub u64);
| ^^^^^^^^
help: consider using `Result::expect` to unwrap the `Result<u64, ParseIntError>` value, panicking if the value is a `Result::Err`
|
13 | Quantity("-100".parse::<u64>().expect("REASON")),
| +++++++++++++++++
error: aborting due to 1 previous error
是的,它确实有帮助,我不能只是盲目地转换。
该死,你真棒。
作为此 API 的用户,这个错误应该足以让我明白我应该优雅地处理这种可能性,也许可以一直返回到 UI,说“请不要使用负数”。 但是如果我继续添加它告诉我的 .expect()
,然后用户输入一个负数呢? 那么我会得到一个运行时崩溃。 比破产好? 我会这么说。
> ./order-6
GOOG false 100 1000
thread 'main' panicked at order/order-6.rs:16:18:
Quantities cannot be negative: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
结束语
你知道这篇文章最有趣的部分是什么吗? Rust 最出名的东西,内存安全,根本没有出现。 当然,你可以争辩说将整数与浮点数混合是一个内存问题,但你会夸大大多数人对内存安全的定义。
我们从这个练习中学到的是,一个设计良好的语言可以通过多种方式保护你免受错误的影响,而不仅仅是阻止你编写 use after free 或数据竞争。 该设计可以通过不强迫你考虑如何保护你的代码免受最简单的错误的影响来节省你大量的脑力——该语言会为你提供支持。
现在,让我们诚实一点。 作为 Rust 初学者,你节省的大部分脑力将用于弄清楚如何说服 borrow checker 你所做的事情是正确的。 它会变得更好,我保证。 但我不会骗你:你使用 borrow checker 的头几个月将会很糟糕。
我们在这里结束了吗? 并非如此。 Matt 在他的演讲中还介绍了另外两个主题,我将在以后的文章中讨论,其中一个与我对 C++ 最大的不满有关。 对于那些正在思考“如果 C++ 有几十年的时间学习,它也会有设计得如此精良的功能”的人,好吧……我们只能说你将会大吃一惊。
再次强调,去看看 Matt Godbolt。 听听这个人说的话,阅读他写的所有内容,观看他在 YouTube 上的所有演讲视频,并使用他的 Compiler Explorer。 你会学到很多东西,并且在做的过程中会感到快乐!
相关文章
##### Persian Rug, Part 4 - The limitations of proxies
##### Persian Rug, Part 3 - The warp and the weft
##### Persian Rug, Part 2 - Other ways to make object soups in Rust