亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?
2024年10月07日 11:57 CSDN

【CSDN 编者按】随着编程语言的不断发展,Rust 和即将推出的 C++26 在代码生成领域的对比越来越受到开发者和研究者的关注。本文作者身为C++ 标准委员会成员,将重点讨论 Rust 的过程宏并分析其工作原理,并基于此展示其是如何为 C++26 提出截然不同的解决方案的。

原文链接:https://brevzin.github.io/c++/2024/09/30/annotations/

作者 | Barry Revzin,C++ 标准委员会成员   翻译 | 郑丽媛

出品 | CSDN(ID:CSDNnews)

我很喜欢做的一件事,就是比较不同编程语言如何解决相同的问题,尤其是当这些语言采取了截然不同的方法时,我觉得这非常具有教育意义。在这篇文章中,我们将尝试把反射(reflection)这一颠覆性的语言特性引入到 C++26 标准中。从根本上来讲,反射可以分为两大部分:

1、自省(Introspection):在编译期间,能够对程序进行查询的能力。

2、代码生成(Code Generation):让程序自动生成新代码的能力。

针对 C++26 的 P2996 提案是一个处理自省问题的核心提案,它为未来扩展反射功能奠定了基础,涵盖多个方向的延展功能(例如 P3294 的代码生成设计)。然而,虽然自省功能本身非常有用,但它只解决了一半的问题——知名 C++ 技术专家 Andrei Alexandrescu 甚至在 CppCon 大会上宣称,如果没有代码生成,自省几乎是“无用的”。

目前,C++ 确实有一种代码生成功能:C 宏(C Macros)。不过,这种机制非常原始,且存在许多局限。首先,C 宏缺乏严格的语法规则,甚至可能在不知情的情况下调用宏(标准库实现对此有保护措施)。其次,实现一些简单的逻辑(如迭代或条件判断)往往需要相当复杂的技巧。然而,尽管存在这些问题,在某些场景下,C 宏仍然是最好的解决方案——这也反映了我们迫切需要更完善的代码生成机制。

另一方面,Rust 虽然没有任何自省功能,但它拥有成熟的代码生成机制,特别是其声明式和过程宏。因此本文将重点讨论 Rust 的过程宏,尤其是派生宏(derive macro)。我们将通过两个示例展示派生宏如何解决问题,分析其工作原理,以及我们如何为 C++26 提出截然不同的解决方案。

不过,我不是专业 Rust 程序员,因此如果我在文中犯了错误,还请大家指正。更新一下,在发布这篇博客后,有人指出我在一些地方犯了错误,我已经进行了更正。这些错误包括:我曾提到 Rust 属性无法接受任意值(其实它是可以的,只是旧版本选择不这么做),以及有比我提到的更好的方式来解析属性(实际上大多数人都采用类似做法)。

结构体的美化打印(Pretty-Printing)

当你学会如何声明一个带有新成员的类型后,很可能会想让这个类型进行调试打印(debug-printable)。不仅是因为调试打印在日常开发中非常有用,还因为在 Rust 中实现这一功能非常简单:

#[derive(Debug)]

struct Point {

x: i32,

y: i32,

}

fn main() {

let p = Point { x: 1, y: 2 };

// prints: p=Point { x: 1, y: 2 }

println!("p={p:?}");

}

代码的第一行通过 #[derive(Debug)] 让 Point 结构体支持调试打印。它的作用是自动生成代码,使得可以打印类型名称以及所有成员的名称和值,并按顺序输出。

在我手头的《Rust 编程语言》书中,第 82 页就展示了如何声明一个 struct,第 89 页则展示了如何让它支持调试打印,这几乎是 Rust 学习过程中最早会遇到的功能之一。对于这个任务,Rust 还提供了另一个简便的方式:dbg!(p),不过这里我使用 println! 是为了更贴近未来在 C++ 中实现类似功能的方式。

由于这是编译时的注解(annotation),如果以后我为 Point 结构体添加了一个新字段(比如我决定将其扩展为三维结构体,添加一个 z 字段),调试打印的输出也会自动更新,以打印新字段的值。

总结来说就是:非常简单!

你可能会问,这究竟是如何实现的?是什么使得宏和 Debug 特性(trait)能够实现这种交互?正如我之前提到的,不同于我们为 C++26 提出的方案,Rust 没有任何形式的自省(introspection)功能,也没有机制可以查询结构体的成员并对其进行迭代。

相反,Rust 的 derive 宏采用了非常不同的方式:它是一个函数,接收被注解结构体的 Token 流作为输入,生成相应的 Token 代码并注入到代码中。实际上,这些注入的代码并不一定与输入直接相关。

在这种情况下,我们通过获取 Point 结构体的 Token 输入,解析它,并使用解析结果生成我们需要的输出,而绕过了缺乏自省的问题。我想这也算是一种“自省”——只不过它只能在特定情况下明确选择使用。

在上面的例子中,derive 宏生成了如下代码(我使用 cargo expand 得到的结果):

#[automatically_derived]

impl ::core::fmt::Debug for Point {

#[inline]

fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {

::core::fmt::Formatter::debug_struct_field2_finish(

f,

"Point",

"x",

&self.x,

"y",

&&self.y,

)

}

}

这个代码看起来并不复杂,但关键在于 Rust 程序员无需手动编写这些模板代码。他们只需要学习如何写一行代码(实际上,连一整行都不需要):#[derive(Debug)]。这就是代码生成的强大之处。

即便如此,这个结果也很有趣。为什么对 self.x 使用 &self.x,而对 self.y 使用 &&self.y 呢?这与 Rust 无法进行自省功能有关。在 Rust 中,最后一个字段可以是不定长类型(unsized type)。不定长类型可以被打印,但是需要一个额外的间接层。derive 宏无法知道 y 是否是定长的(在这个例子中它是 i32所以是定长的),所以为了支持两种情况,宏预先添加了这个额外的间接层。

在 C++ 中,如果想要提出的方案尽量贴近 Rust 的语法,可以这样实现:

struct [[=derive

]] Point {

int x;

int y;

};

int main() {

auto p = Point{.x=1, .y=2};

// prints p=Point{.x=1, .y=2}

std::println("p={}", p);

}

从本质上讲,C++ 和 Rust 的格式化机制有一些相似之处在 Rust 中,你必须为 Debug trait 提供一个 impl。而在 C++ 中,你需要特化 std::formatter(我们不区分 Debug 和 Display)。正如我之前展示的,Rust 的宏调用会为类型注入正确的 impl Debug 代码,而在 C++ 中,我们并没有这样做。

我在这里使用的特性叫做“注解”(annotation),这个功能将在 P3394 提案中提出,首次由 Daveed Vandevoorde 在 CppCon 的闭幕演讲中披露。这个提案的目标是让你能够以一种自省可以观察到的方式标注声明。值得注意的是,这里并没有发生任何代码注入,我们只是稍微扩展了一下自省功能。

然而,鉴于 C++ 本身具备自省功能(或者将随着 P2996 的提出而获得),这已经足够完成我们的目标。我们可以提前提供一个特化的 std::formatter,该特化会在类型带有 derive

注解时

启用,而这个注解本质上只是一个空值:

template

struct Derive { };

template

inline constexpr Derive

derive;

inline constexpr struct{} Debug;

template

requires (has_annotation(^^T, derive

))

struct std::formatter

{

// ...

};

一旦我们有了这个基础,特化的实现就可以对类型 T 进行自省,获取我们需要的所有信息,以便展示:我们可以迭代所有非静态的数据成员,格式化它们的名称和值。一个简化的实现如下:

template

requires (has_annotation(^^T, derive

))

struct std::formatter

{

constexpr auto parse(auto& ctx) { return ctx.begin(); }

auto format(T const& m, auto& ctx) const {

auto out = std::format_to(ctx.out(),

"{}", display_string_of(^^T));

*out++ = '{';

bool first = true;

[:expand(nonstatic_data_members_of(^^T)):] >> [&]

{

if (not first) {

*out++ = ',';

*out++ = ' ';

}

first = false;

out = std::format_to(out,

".{}={}",

identifier_of(nsdm), m.[:nsdm:]);

};

*out++ = '}';

return out;

}

};

某种意义上来说,我们仍然是在生成代码——模板实际上就是 C++ 中的一种代码生成形式。但有趣的是,在这里我们通过非常不同的机制实现了相同的目标。

请注意,这就是完整的实现代码,可以看到代码量其实并不多。

JSON 序列化

在之前讨论的调试打印示例中,我们只是简单地按顺序打印所有成员。那么如果我们想做些更复杂的操作呢?在处理序列化时,有时字段的名称可能需要与原始的成员名不同。还有些情况,目标格式在编程语言中根本无法直接表达——比如字段名可能是语言中的关键字,或者字段名包含空格等等。

因此,Rust 的 serde 库提供了许多注解属性,可以添加到类型和成员上,以控制序列化逻辑。下面是一个简单的例子:

use serde::Serialize;

use serde_json;

#[derive(Serialize)]

struct Person {

#[serde(rename = "first name")]

first: String,

#[serde(rename = "last name")]

last: String,

}

fn main() {

let person = Person {

first: "Peter".to_owned(),

last: "Dimov".to_owned(),

};

let j = serde_json::to_string(&person).unwrap();

// prints {"first name":"Peter","last name":"Dimov"}

println!("{}", j);

}

类似于 Debug 特性,Serialize 的派生宏会为我们注入一个实现,其生成的代码如下:

#[doc(hidden)]

#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]

const _: () = {

#[allow(unused_extern_crates, clippy::useless_attribute)]

extern crate serde as _serde;

#[automatically_derived]

impl _serde::Serialize for Person {

fn serialize

(

&self,

__serializer: __S,

) -> _serde::__private::Result

where

__S: _serde::Serializer,

{

let mut __serde_state = _serde::Serializer::serialize_struct(

__serializer,

"Person",

false as usize + 1 + 1,

)?;

_serde::ser::SerializeStruct::serialize_field(

&mut __serde_state,

"first name",

&self.first,

)?;

_serde::ser::SerializeStruct::serialize_field(

&mut __serde_state,

"last name",

&self.last,

)?;

_serde::ser::SerializeStruct::end(__serde_state)

}

}

};

在这里,你可以看到想要序列化的字段名(如 "first name""last name")与实际的成员绑定在一起。需要注意的是,false as usize + 1 + 1是用来表示要序列化的字段数量的构造,这里的 2 显然是字段的数量。

如果我们要添加一个中间名,并且只有当它非空时才进行序列化,可以使用 skip_serializing_if 属性:

#[derive(Serialize)]

struct Person {

#[serde(rename = "first name")]

first: String,

#[serde(rename = "middle name", skip_serializing_if = "String::is_empty")]

middle: String,

#[serde(rename = "last name")]

last: String,

}

生成的代码如下,具体新增部分为第 18-19 行、第 26-37 

#[doc(hidden)]

#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]

const _: () = {

#[allow(unused_extern_crates, clippy::useless_attribute)]

extern crate serde as _serde;

#[automatically_derived]

impl _serde::Serialize for Person {

fn serialize

(

&self,

__serializer: __S,

) -> _serde::__private::Result

where

__S: _serde::Serializer,

{

let mut __serde_state = _serde::Serializer::serialize_struct(

__serializer,

"Person",

false as usize + 1 + if String::is_empty(&self.middle) { 0 } else { 1 }

+ 1,

)?;

_serde::ser::SerializeStruct::serialize_field(

&mut __serde_state,

"first name",

&self.first,

)?;

if !String::is_empty(&self.middle) {

_serde::ser::SerializeStruct::serialize_field(

&mut __serde_state,

"middle name",

&self.middle,

)?;

} else {

_serde::ser::SerializeStruct::skip_field(

&mut __serde_state,

"middle name",

)?;

}

_serde::ser::SerializeStruct::serialize_field(

&mut __serde_state,

"last name",

&self.last,

)?;

_serde::ser::SerializeStruct::end(__serde_state)

}

}

};

但在 C+中,我们并没有像 serde 这样的库,它可以分离序列化的字段名和成员变量的名称至少我目前不知道有这样的库。C++ 中,通常是 JSON 库处理 JSON 序列化,TOML 库处理 TOML 序列化等。也许这是因为 C++ 缺乏像 Rust 那样的语言支持,所以无法轻松实现这种序列化机制?

老实说,虽然在格式化方面 Rust 和 C++ 的实现有些相似,但在序列化的灵活性上 Rust 确实更具优势。尽管 C++ 中没有完全类似 serde 的库,但我们也可以使用类似 Boost.JSON 这样的库来实现序列化。

我们从支持 derive

和 rename 开始,这是为了让代码能够正常工作的全部需求:

struct [[=derive

]] Point {

int x, y;

};

struct [[=derive

]] Person {

[[=serde::rename("first name")]] std::string first;

[[=serde::rename("last name")]] std::string last;

};

int main() {

// prints {"x":1,"y":2}

std::cout

// prints {"first name":"Peter","last name":"Dimov"}

std::cout

}

整段代码只有 21 行,如果我保持与之前相同的模板形式,那么基本通过 derive 可以实现:

namespace serde {

inline constexpr struct{} Serialize{};

struct rename { char const* field; };

}

namespace boost::json {

template

requires (has_annotation(^^T, derive

))

void tag_invoke(value_from_tag const&, value& v, T const& t) {

auto& obj = v.emplace_object();

[:expand(nonstatic_data_members_of(^^T)):] >> [&]

{

constexpr auto field = annotation_of

(M)

.transform([](serde::rename r){

return std::string_view(r.field);

})

.value_or(identifier_of(M));

obj[field] = boost::json::value_from(t.[:M:]);

};

}

}

这段代码应该看起来很熟悉,因为它基本上也是在做格式化工作,只不过这里我们是将成员添加到一个 JSON 对象中,而不是打印一堆键值对。然后,我们没有自动使用非静态数据成员的标识符,而是先尝试检查是否有 rename 注解。annotation_of

()

 为我们提供了一个 optional因此我们要么获取 rename 注解的字段名(及其底层字符串),要么回退到 identifier_of(M)

在这里添加 skip_serializing_if 的支持并不需要太多额外的工作,这也很好地展示了 C++ 和 Rust 处理方式之间的区别。在 Rust 中,你提供一个字符串,它会被注入并在内部调用;而在 C++ 中,我们通常会直接提供一个可调用对象。

起初我以为这是因为 Rust 的属性语法不支持在这里使用可调用对象,但实际上似乎是因为 serde 在支持这一点之前就已经存在了。

我们需要为此添加一个新的注解类型:

namespace serde {

inline constexpr struct{} Serialize{};

struct rename { char const* field; };

template

struct skip_serializing_if { F pred; };

}

然后稍微麻烦一点的部分是对它的解析,我们需要提取出某个 serde::skip_serializing_if 的特化类型注解。如果找到了,就尝试调用其 pred 成员函数,若该函数返回 true,就跳过该字段的序列化。

搜索过程如下所示(注意,我们需要使用 constexpr,因为需要拼接它以进行调用)。我确信这个部分可以通过更好的库 API 稍作清理(至少可以用一个 std::optional 来改进):

constexpr auto skip_if = []() -> std::meta::info {

auto res = std::meta::info();

for (auto A : annotations_of(M)) {

auto type = type_of(A);

if (has_template_arguments(type)

and template_of(type) == ^^serde::skip_serializing_if) {

// found a specialization

// but check to make sure we haven't found two

// different ones.

if (res != std::meta::info() and res != value_of(A)) {

throw "unexpected duplicate";

}

res = value_of(A);

}

}

return res;

}();

然后,如果我们有这样的注解,就调用它来确定是否需要跳过这个成员。这里需要用 if constexpr 语句,因为如果 skip_if 是空反射,我们无法对其进行拼接。除此之外,整体逻辑非常简单:如果有这样的注解,就调用它,如果返回 false,则跳过这个成员:

if constexpr (skip_if != std::meta::info()) {

if (std::invoke([:skip_if:].pred, t.[:M:])) {

return;

}

}

现在这段代码已经膨胀到了 51 行(新增部分为第 7 行、第 22-46 行):

template

struct Derive { };

template

inline constexpr Derive

derive;

namespace serde {

inline constexpr struct{} Serialize{};

struct rename { char const* field; };

template

struct skip_serializing_if { F pred; };

}

namespace boost::json {

template

requires (has_annotation(^^T, derive

))

void tag_invoke(value_from_tag const&, value& v, T const& t) {

auto& obj = v.emplace_object();

[:expand(nonstatic_data_members_of(^^T)):] >> [&]

{

constexpr auto field = annotation_of

(M)

.transform([](serde::rename r){

return std::string_view(r.field);

})

.value_or(identifier_of(M));

constexpr auto skip_if = []() -> std::meta::info {

auto res = std::meta::info();

for (auto A : annotations_of(M)) {

auto type = type_of(A);

if (has_template_arguments(type)

and template_of(type) == ^^serde::skip_serializing_if) {

// found a specialization

// but check to make sure we haven't found

// two different ones.

if (res != std::meta::info() and res != value_of(A)) {

throw "unexpected duplicate";

}

res = value_of(A);

}

}

return res;

}();

if constexpr (skip_if != std::meta::info()) {

if (std::invoke([:skip_if:].pred, t.[:M:])) {

return;

}

}

obj[field] = boost::json::value_from(t.[:M:]);

};

}

}

此时,我想到了解决此问题的另一种有趣方法。只有两个属性的时候这样做可能没有必要,但如果我打算实现 serde 的全部功能有一个不单独处理每个属性解析的策略可能会更加合理那么,如果我们将所有属性收集到一个类类型中,再使用这个类类型会怎样呢?

让我们看看这会是什么样子。

首先,我们创建一个新的类类型——attributes。我们将编程定义它,给它一个每个属性都对应的成员,此时难点在于成员的类型。对于像 serde::rename 这样的属性,我们应该使用 optional。但对于 skip_serializing_if 呢?我们还不知道该使用什么类型,所以这里先用 optional 来进行类型擦除。也就是说,我们希望生成这样的类型:

struct attributes {

optional

rename;

optional

skip_serializing_if;

};

这段代码使用了 std::meta::define_class(),这是 P2996 中唯一一个用于代码生成的 API。它功能不多,但对当前需求来说足够用了。注意,由于我们遍历了命名空间 serde 中的所有成员,需要确保排除 attributes——它当然也在这个命名空间中:

struct attributes;

consteval {

std::vector

specs;

for (auto m : members_of(^^serde)) {

if (m == ^^attributes or not has_identifier(m)) {

continue;

}

auto underlying = is_type(m) ? m : ^^std::meta::info;

specs.push_back(data_member_spec(

substitute(^^std::optional, {underlying}),

{.name=identifier_of(m)}));

}

define_class(^^attributes, specs);

};

然后我们可以编写一个解析函数,将非静态数据成员的属性写入 attributes 实例中。这里最麻烦的部分就是找到写入 attributes 哪个非静态数据成员。我们暂时跳过这部分逻辑,直接进入如何利用这些工作成果

namespace boost::json {

template

requires (has_annotation(^^T, derive

))

void tag_invoke(value_from_tag const&, value& v, T const& t) {

auto& obj = v.emplace_object();

[:expand(nonstatic_data_members_of(^^T)):] >> [&]

{

constexpr auto attrs = serde::parse_attrs_from

();

constexpr auto field = attrs.rename

.transform([](serde::rename r){

return std::string_view(r.field);

})

.value_or(identifier_of(M));

if constexpr (attrs.skip_serializing_if) {

if (std::invoke(

[:*attrs.skip_serializing_if:].pred,

t.[:M:]))

{

return;

}

}

obj[field] = boost::json::value_from(t.[:M:]);

};

}

}

当然,我们将最复杂的逻辑(解析注解)移到了一个函数中,而这个函数我没有包含在上面的代码块中。如我所说,对于只有两个属性的情况,这样做可能有点大材小用。不过,这种方法意味着添加一个新属性只需在命名空间 serde 中声明一个新类或类模板,然后在实现中使用它即可。

Rust 属性 vs. C++ 注解

在对比 C++ 和 Rust 中的 serde 解决方案时,有两个方面引起了我的注意:语法和库设计。

语法

首先从语法差异来看,使用时的体验是我最先关注的点。以下是我在 Rust 中的声明:

#[derive(Serialize)]

struct Person {

#[serde(rename = "first name")]

first: String,

#[serde(rename = "middle name", skip_serializing_if = "String::is_empty")]

middle: String,

#[serde(rename = "last name")]

last: String,

}

而这是我在 C++ 中的声明:

struct [[=derive

]] Person {

[[=serde::rename("first name")]]

std::string first;

[[=serde::rename("middle name")]]

[[=serde::skip_serializing_if(&std::string::empty)]]

std::string middle = "";

[[=serde::rename("last name")]]

std::string last;

};

可以看到,C++ 的注解语法显得更为复杂和冗长,而这大多是由于语法本身的问题。相较之下 Rust 的注解较为简洁,因为它们遵循不同于语言其余部分的语法规则 —— 比如 serde(rename = "first name") 在 Rust 中是无效的,这里也没有调用名为 serde 的函数。

这种差异带来的好处是,Rust 中的注解使用起来更加清晰自然,因为它真的就像是给选项赋值一样。例如,类似于 serde(rename = "first name") 这样的用法更像是传递配置参数,而不是在调用函数。这为使用者提供了灵活性,比如可以像这样使用属性:#[arg(short)] 或 #[arg(short = 'k')],前者使用了默认值,而后者显式指定了 'k'。

看到这里,你可能会有一种冲动,想要重用(非常特殊且具体的)属性语法,并允许在 C++ 中使用 using 关键字。但实际上,这样做并不会节省太多的输入:

struct [[=derive

]] Person {

// old version: 83 chars

[[=serde::rename("middle name"), =serde::skip_serializing_if(&std::string::empty)]]

std::string middle = "";

// new version: 82 chars

[[using serde: =rename("middle name"), =skip_serializing_if(&std::string::empty)]]

std::string middle = "";

};

相比之下,Rust 版本只有 74 个字符。虽然长度上的差异并不大,但至少它少于 80 个。

另一方面,要关注 Rust 为实现这一点付出了什么代价。在 C++ 注解设计中,注解本质上就是值。你需要学习的新语法很少,还可以很清楚地看到这里发生了什么。注解的内容并不是由库定义含义的咒语,而是实际的 C++ 值。如果你不知道 serde::skip_serializing_if 是什么意思,可以直接查看它的定义。

你可能会注意到,在讨论这些示例实现时,我没有提到如何从注解中解析出值——这是因为实际上我不需要做任何解析,编译器为我完成了这项工作我唯一需要做的,就是从注解列表中提取我关心的注解,这并不涉及实际的解析过程。而 Rust 库则必须真正解析这些 Token 流,对于 serde 来说,这意味着接近 2000 行代码。

另一个有趣的事情是,管 Rust 和 C++ 最终以不同的方式实现了相似的功能,但它们并不完全相同。在 Rust 中,#[derive(Debug)] 会为类型注入适当的 impl Debug。而在 C++ 的注解方法中,我们并没有注入适当的 formatter 特化,只是添加了一个全局约束的版本。

这意味着,如果不做进一步处理,仅仅做一个小小的改动就可能导致歧义:

struct [[=derive

]] Point {

int x;

int y;

// let's just make this a range for seemingly no reason

auto begin() -> int*;

auto end() -> int*;

};

int main() {

auto p = Point{.x=1, .y=2};

std::println("p={}", p); // error: ambiguous

}

嗯,我需要做两个小小的改动。我原本的特化定义如下:

template

requires (has_annotation(^^T, derive

))

struct std::formatter

{ /* ... */ };

但如果我将其改为:

template

requires (has_annotation(^^T, derive

))

struct std::formatter

{ /* ... */ };

那么它就可能与 C++23 新增的用于范围的 std::formatter 特化产生歧义。为了解决这个问题,可以禁用一个额外的变量模板(在链接中会被预处理掉):

template

requires (has_annotation(^^T, derive

))

inline constexpr auto std::format_kind

= std::range_format::disabled;

这似乎有点令人意外——因为从概念上讲,C++ 的方法与 Rust 的方法是相同的,添加注解会注入一个非常特定且明确的特化,这不可能与其他内容产生歧义——但事实并非如此。因此,种部分特化的歧义肯定会成为一个问题。或许在未来,我们可以想出一种方法,使诸如 [[=derive

]] 这样的注解能够真正注入一个特化来避免这个问题。

库设计

在 Rust 的 serde 库中,序列化是一个两阶段的过程。首先,类型作者选择参与序列化,这会生成一个类似于该类型即时表示的实现。然后,不同协议的作者可以有效地实现不同的后端。

例如在 Person 类型的 serde 实现中,Rust 会生成一个 Serialize 实现,该实现接受满足 serde::Serializer任意类型。然后我们对这个 serializer 进行一系列的序列化调用,这些调用会根据协议需求(比如 JSON、CBOR、YAML、TOML 等)执行相应的操作。

如果我们将这种实现方式转换为 C++,看起来可能会像这样(为避免陷入不相关的错误处理细节,这里假设这些函数在出现错误时抛出异常,而不是像 Rust 中那样返回 Result):

template

auto serialize(Person const& p, S& serializer) -> void {

auto state = serializer.serialize_struct(

"Person",

2 + (p.middle.empty() ? 0 : 1));

state.serialize_field("first name", p.first);

if (not p.middle.empty()) {

state.serialize_field("middle name", p.middle);

} else {

state.skip_field("middle name", p.middle);

}

state.serialize_field("last name", p.last);

state.end();

}

这种设计允许解耦,非常不错。然而你可能注意到了,我之前展示的 C++ 实现根本没有这样做。并不是因为我懒,而是因为在有省(introspection)的情况下,这样的操作完全没有必要。在 C++ 中,我们不需要生成这种中间表示,Boost.JSON 实现可直接从数据成员完成所有的序列化工作。

这不仅仅是代码量减少的问题,更重要的是根本不需要处理额外的抽象层。这个抽象层虽然不会消耗太多计算资源,也很容易被编译优化掉,但它本身就是不必要的。

接下来再考虑 skip_field 调用。对于很多序列化目标(例如 JSON),跳过某个字段的方法就是简单地不对其进行序列化。这也是为什么 skip_field 的默认实现什么都不做,serde_json 也没有覆盖这个函数。同样,考虑上面提到的字段数量计算。JSON 序列化器也不需要这样的值,因此它会忽略这个字段类型名称的值。

但在创建中间表示时,你需要创建一个足够丰富的表示来处理所有可能的序列化/反序列化目标。某些序列化目标可能需要预先知道字段数量,或者需要为跳过的字段预留位置。因此,serde 必须为此提供支持。

而在 C++ 中,我们根本不需要这样做。对于任何给定的目标,序列化器可以直接执行它所需的所有操作,因为它直接访问所有信息,不需要额外的抽象层。因此,C++ 版本的 serde 库可能只需要一系列可作为注解的类型、parse_attrs_from() 函数,以及几个小的辅助函数即可

这并不是终点

最后,我想指出几种不同语言中的一些相关特性来结束这篇文章:

  • Rust 的过程宏(procedural macros)

  • Python 的装饰器(decorators)

  • Herb Sutter 的元类(metaclasses)提案

它们都有一个共同点:编写代码,然后将代码传递给一个函数,以生成新的代码。元类和装饰器实际上会替换原始代码,而 derive 宏只会注入新代码(尽管其他过程宏也可以替换代码)。

注解提案在大体上看起来与这些特性类似,但它是一个完全不同的机制,不应与它们混淆:注解并不会注入代码,它只是增强了类型的自省能力。但这并不是说注解没用!正如我所展示的那样,注解有望成为一个非常有用的工具,可以编写出以前在 C++ 中无法想象的用户友好型库 API。

但这仅仅是一个开始。

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部