C++26 Annotations: Metadata for Static Reflection
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 来达到自动化。