A quick recap

Annotations 能够给代码结构附上一些额外信息,供 Reflection 随后查找。

直接上个例子:

struct label {
    int value;
};

inline constexpr label answer{42};

[[=answer]]
int x;

consteval label annotation_value() {
    auto annotations = std::meta::annotations_of(^^x);
    return std::meta::extract<label>(annotations[0]);
}

int main() {
    constexpr auto count = std::meta::annotations_of(^^x).size();
    constexpr auto value = annotation_value();

    // 1
    std::println("annotation count = {}", count);
    // 42
    std::println("annotation value = {}", value.value);
}

[[=answer]] 把一个编译期值附加到变量 x 上,annotations_of(^^x) 通过反射找到它,extract<label> 再把反射元信息还原成附加的值。

只要是编译期的值,都可以作为这个额外附加的信息,而 Constant Expressions 从 C++11 发展到 C++26,已经具备不错的基础功能,能够表达形式多样的额外信息。

Tool Calling

Agent Framework 里面的工具调用模块就需要用到反射,工具需要增加描述信息,这就是典型的需要附加信息的场景。没有 C++26 Annotations 这个特性,语法上根本无法表达这种额外信息,当初用 C++20 实现 Wuwe Agent Frameworks 时,我是通过这种语法形式表达的:

struct get_weather {
  std::string_view description = "Get the current weather for a given city.";

  std::string city;

  std::string invoke() const{
    if (city == "Tokyo") {
      return "Tokyo is 22C and cloudy.";
    }
    if (city == "Shanghai") {
      return "Shanghai is 24C with light rain.";
    }
    return city + " weather data is unavailable in this demo.";
  }
};

然而,这是成员变量,并不是真正意义上的附加信息,而借助 C++26 Annotations,实现逻辑更加自然,变为:

struct description {
    static constexpr std::size_t capacity = 128;

    char text[capacity + 1]{};

    template <std::size_t N>
    constexpr description(const char (&str)[N]) {
        static_assert(N <= capacity + 1, "description is too long");
        std::copy_n(str, N, text);
    }

    constexpr std::string_view view() const noexcept {
        return text;
    }
};

struct [[=description("Get the current weather for a given city.")]]
get_weather {
    std::string city;

    std::string invoke() const {
        if (city == "Tokyo") {
            return "Tokyo is 22C and cloudy.";
        }
        if (city == "Shanghai") {
            return "Shanghai is 24C with light rain.";
        }
        return city + " weather data is unavailable in this demo.";
    }
};

consteval description get_description() {
    auto annotations = std::meta::annotations_of(^^get_weather);
    return std::meta::extract<description>(annotations[0]);
}

int main() {
    constexpr auto desc = get_description();

    std::println("description: {}", desc.view());
}

这里依旧用到了一个技巧,即定长数组,如果直接用不定长度的字符数组,extract 的时候没办法知道 description 的具体类型。

为什么不直接使用 std::string_view 呢?只因它不是 Structural type,无法在此场景下使用。

has_annotation

这个函数需要自己实现,用来判断是否存在某个注解信息。实现很简单:

template <class Annotation>
consteval bool has_annotation(std::meta::info entity) {
    return !std::meta::annotations_of_with_type(entity, ^^Annotation).empty();
}

consteval bool has_description(std::meta::info entity) {
    return has_annotation<description>(entity);
}

int main() {
    if (has_description(^^get_weather)) {
        constexpr auto desc = get_description();
        std::println("description: {}", desc.view());
    }
}

More Than Tags

我们还可以把函数指针作为 Annotations,从而实现更灵活的注解场景。

一个序列化、过滤空值成员的示例:

#include <meta>
#include <print>
#include <string>

namespace serde {

struct skip_serializing_if {
    using predicate_type = bool (std::string::*)() const noexcept;

    predicate_type pred;

    constexpr skip_serializing_if(predicate_type p) noexcept
        : pred(p) {}

    friend constexpr bool operator==(
        skip_serializing_if const&,
        skip_serializing_if const&
    ) = default;
};

} // namespace serde

struct person {
    std::string first;

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

    std::string last;
};

consteval bool has_skip_serializing_if(std::meta::info member) {
    auto annotations = std::meta::annotations_of_with_type(
        member,
        ^^serde::skip_serializing_if
    );

    return !annotations.empty();
}

consteval serde::skip_serializing_if
get_skip_serializing_if(std::meta::info member) {
    auto annotations = std::meta::annotations_of_with_type(
        member,
        ^^serde::skip_serializing_if
    );

    if (annotations.empty()) {
        throw "missing serde::skip_serializing_if annotation";
    }

    return std::meta::extract<serde::skip_serializing_if>(annotations[0]);
}

template <class T>
void serialize(T const& obj) {
    std::println("{{");

    static constexpr auto members = std::define_static_array(
        std::meta::nonstatic_data_members_of(
            ^^T,
            std::meta::access_context::unchecked()
        )
    );

    template for (constexpr std::meta::info member : members) {
        if constexpr (has_skip_serializing_if(member)) {
            constexpr auto rule = get_skip_serializing_if(member);

            if ((obj.[:member:].*(rule.pred))()) {
                continue;
            }
        }

        std::println("  \"{}\": \"{}\",",
                     std::meta::identifier_of(member),
                     obj.[:member:]);
    }

    std::println("}}");
}

int main() {
    person p1{
        .first = "John",
        .middle = "",
        .last = "Smith"
    };

    person p2{
        .first = "John",
        .middle = "William",
        .last = "Smith"
    };

    std::println("p1:");
    serialize(p1);

    std::println();

    std::println("p2:");
    serialize(p2);
}

这个示例的 Annotations 是 [[=serde::skip_serializing_if(&std::string::empty)]],一个成员函数指针。随后通过反射拿到该函数地址 obj.[:member:].*(rule.pred),实现调用 obj 成员函数的功能。

所以,为空值的成员,以这种方式被过滤了。输出为:

p1:
{
  "first": "John",
  "last": "Smith",
}

p2:
{
  "first": "John",
  "middle": "William",
  "last": "Smith",
}

总之,Annotations 可以携带强类型的 Callable,只要它是一个编译期的结构化值(带捕获的 Lambda 和 std::function 就不行)。

Conclusion

C++26 Annotations 是个无法通过其他手段实现的新特性,是一种可被反射读取的注解机制。它可以给声明挂上一个编译期的值,再通过反射读取到该值,以驱动代码生成、序列化、命令行解析、测试框架等需要灵活控制的场景。

以往实现框架时,有些不可能消除的代码,如人工注册路由、人工同步参数类型等,如今都能够通过 Annotations 来达到自动化。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.