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

C语言与CPP编程 2024-10-31 09:02

点击上方“C语言与CPP编程”,选择“关注/置顶/星标公众号

干货福利,第一时间送达!

最近有小伙伴说没有收到当天的文章推送,这是因为微信更改了推送机制,导致没有星标公众号的小伙伴刷不到当天推送的文章,无法接收到一些比较实用的知识和资讯。所以建议大家加个星标⭐️,以后就能第一时间收到推送了。

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

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


我很喜欢做的一件事,就是比较不同编程语言如何解决相同的问题,尤其是当这些语言采取了截然不同的方法时,我觉得这非常具有教育意义。在这篇文章中,我们将尝试把反射(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 属性无法接受任意值(其实它是可以的,只是旧版本选择不这么做),以及有比我提到的更好的方式来解析属性(实际上大多数人都采用类似做法)。


1、结构体的美化打印(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 <auto V> struct Derive { };template <auto V> inline constexpr Derive derive;
inline constexpr struct{} Debug;
template <class T> requires (has_annotation(^^T, derive))struct std::formatter { // ...};

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

template <class T> 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)):] >> [&]<auto nsdm>{ if (not first) { *out++ = ','; *out++ = ' '; } first = false;
out = std::format_to(out, ".{}={}", identifier_of(nsdm), m.[:nsdm:]); };
*out++ = '}'; return out; }};

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

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


2、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<__S>(            &self,            __serializer: __S,        ) -> _serde::__private::Result<__S::Ok, __S::Error>        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<__S>(            &self,            __serializer: __S,        ) -> _serde::__private::Result<__S::Ok, __S::Error>        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 << boost::json::value_from(Point{.x=1, .y=2}) << '\n'; // prints {"first name":"Peter","last name":"Dimov"} std::cout << boost::json::value_from(Person{.first="Peter", .last="Dimov"}) << '\n';}

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

namespace serde {    inline constexpr struct{} Serialize{};    struct rename { char const* field; };}
namespace boost::json { template <class T> 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)):] >> [&]<auto M>{ 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 <class F> 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 <auto V> struct Derive { };template <auto V> inline constexpr Derive derive;
namespace serde { inline constexpr struct{} Serialize{}; struct rename { char const* field; }; template <class F> struct skip_serializing_if { F pred; };}
namespace boost::json { template <class T> 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)):] >> [&]<auto M>{ 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<std::meta::info> 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 <class T>        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 中声明一个新类或类模板,然后在实现中使用它即可。


3、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 <class T> requires (has_annotation(^^T, derive))struct std::formatter { /* ... */ };

但如果我将其改为:

template <class T, class Char> requires (has_annotation(^^T, derive))struct std::formatter { /* ... */ };

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

template <class T> 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() 函数,以及几个小的辅助函数即可


4、这并不是终点

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

  • Rust 的过程宏(procedural macros)

  • Python 的装饰器(decorators)

  • Herb Sutter 的元类(metaclasses)提案

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

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

但这仅仅是一个开始。

本文转自公众号“CSDN”,ID:CSDNnews

本文经授权转自公众号CSDN(ID:CSDNnews)

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


EOF

你好,我是飞宇。日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。

我组建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起进群交流。

欢迎你添加我的微信,我拉你进技术交流群。此外,我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推机会

加个微信,打开另一扇窗

经常遇到有读者后台私信想要一些编程学习资源,这里分享 1T 的编程电子书、C/C++开发手册、Github上182K+的架构路线图、LeetCode算法刷题笔记等精品学习资料,点击下方公众号会回复"编程"即可免费领取~

感谢你的分享,点赞,在看三  

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 86浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 70浏览
  • 在现代科技浪潮中,精准定位技术已成为推动众多关键领域前进的核心力量。虹科PCAN-GPS FD 作为一款多功能可编程传感器模块,专为精确捕捉位置和方向而设计。该模块集成了先进的卫星接收器、磁场传感器、加速计和陀螺仪,能够通过 CAN/CAN FD 总线实时传输采样数据,并具备内部存储卡记录功能。本篇文章带你深入虹科PCAN-GPS FD的技术亮点、多场景应用实例,并展示其如何与PCAN-Explorer6软件结合,实现数据解析与可视化。虹科PCAN-GPS FD虹科PCAN-GPS FD的数据处
    虹科汽车智能互联 2024-11-29 14:35 149浏览
  • 国产光耦合器因其在电子系统中的重要作用而受到认可,可提供可靠的电气隔离并保护敏感电路免受高压干扰。然而,随着行业向5G和高频数据传输等高速应用迈进,对其性能和寿命的担忧已成为焦点。本文深入探讨了国产光耦合器在高频环境中面临的挑战,并探索了克服这些限制的创新方法。高频性能:一个持续关注的问题信号传输中的挑战国产光耦合器传统上利用LED和光电晶体管进行信号隔离。虽然这些组件对于标准应用有效,但在高频下面临挑战。随着工作频率的增加,信号延迟和数据保真度降低很常见,限制了它们在电信和高速计算等领域的有效
    腾恩科技-彭工 2024-11-29 16:11 106浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 57浏览
  • 光耦合器作为关键技术组件,在确保安全性、可靠性和效率方面发挥着不可或缺的作用。无论是混合动力和电动汽车(HEV),还是军事和航空航天系统,它们都以卓越的性能支持高要求的应用环境,成为现代复杂系统中的隐形功臣。在迈向更环保技术和先进系统的过程中,光耦合器的重要性愈加凸显。1.混合动力和电动汽车中的光耦合器电池管理:保护动力源在电动汽车中,电池管理系统(BMS)是最佳充电、放电和性能监控背后的大脑。光耦合器在这里充当守门人,将高压电池组与敏感的低压电路隔离开来。这不仅可以防止潜在的损坏,还可以提高乘
    腾恩科技-彭工 2024-11-29 16:12 117浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 155浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 63浏览
  • 在电子技术快速发展的今天,KLV15002光耦固态继电器以高性能和强可靠性完美解决行业需求。该光继电器旨在提供无与伦比的电气隔离和无缝切换,是现代系统的终极选择。无论是在电信、工业自动化还是测试环境中,KLV15002光耦合器固态继电器都完美融合了效率和耐用性,可满足当今苛刻的应用需求。为什么选择KLV15002光耦合器固态继电器?不妥协的电压隔离从本质上讲,KLV15002优先考虑安全性。输入到输出隔离达到3750Vrms(后缀为V的型号为5000Vrms),确保即使在高压情况下,敏感的低功耗
    克里雅半导体科技 2024-11-29 16:15 119浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 54浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 59浏览
  • By Toradex胡珊逢简介嵌入式领域的部分应用对安全、可靠、实时性有切实的需求,在诸多实现该需求的方案中,QNX 是经行业验证的选择。在 QNX SDP 8.0 上 BlackBerry 推出了 QNX Everywhere 项目,个人用户可以出于非商业目的免费使用 QNX 操作系统。得益于 Toradex 和 QNX 的良好合作伙伴关系,用户能够在 Apalis iMX8QM 和 Verdin iMX8MP 模块上轻松测试和评估 QNX 8 系统。下面将基于 Apalis iMX8QM 介
    hai.qin_651820742 2024-11-29 15:29 150浏览
  • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
    克里雅半导体科技 2024-11-29 16:18 157浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦