Structural Interface(SI)是一个基于 C++26 静态反射的 header-only 结构化接口实验库。它让普通 struct 同时作为泛型约束接口和运行时类型擦除接口使用。
用普通 struct 定义接口。任何满足接口的类型都能擦除为 existential,在同一个容器里动态分发:
// 定义接口:一个包含所需成员的 struct
struct Drawable {
void draw() const;
};
// 满足 Drawable 接口的具体类型
struct Circle {
void draw() const { std::println("Circle"); }
};
struct Rectangle {
void draw() const { std::println("Rectangle"); }
};
static_assert(si::satisfies<Circle, Drawable>);
static_assert(si::satisfies<Rectangle, Drawable>);
int main() {
// 类型擦除容器:任何满足 Drawable 接口的类型都可放入
std::vector<si::existential<Drawable>> shapes{ Circle{}, Rectangle{} };
for (const auto& shape : shapes) {
shape.draw(); // 动态分发
}
}不需要宏、不需要继承基类、不需要代码生成器——只需要 C++26。
只要写一个入参满足 si::satisfies<Drawable> 约束的泛型函数,它就能同时接受具体类型(静态分发)和 existential(动态分发):
// 同一算法函数,同时用于静态分发和动态分发
void render(const si::satisfies<Drawable> auto& obj) {
obj.draw();
}
int main() {
Circle circle{};
render(circle); // 静态分发,零运行时开销
si::existential<Drawable> obj = Rectangle{};
render(obj); // 动态分发,同一函数
}一个接口、一个算法、两种分发模式。
接口可以继承多个父接口,组合后的接口要求同时满足所有父接口(Widget = Drawable ∩ Scalable):
struct Drawable {
void draw() const;
};
struct Scalable {
void scale(float factor);
};
// 组合接口 = Drawable + Scalable
struct Widget : Drawable, Scalable {};
// 同时满足 Drawable 和 Scalable 的具体类型
struct Button {
void draw() const { std::println("Button"); }
void scale(float factor) { std::println(" scaled by {}", factor); }
};
static_assert(si::satisfies<Button, Widget>);
int main() {
std::vector<si::existential<Widget>> widgets{ Button{} };
for (const auto& w : widgets) {
w.draw();
w.scale(2.0f);
}
}接口里的字段在运行时以原始字段指针暴露,用 *(obj.field) 访问:
struct Drawable {
std::string name; // 数据成员
void draw() const; // 函数成员
};
struct Circle {
std::string name;
void draw() const { std::println("{}", name); }
};
struct Rectangle {
std::string name;
void draw() const { std::println("{}", name); }
};
int main() {
std::vector<si::existential<Drawable>> shapes{ Circle{ "circle" }, Rectangle{ "rect" } };
for (const auto& shape : shapes) {
// shape.name 的类型是 std::string*,不是 std::string
std::println("{}:", *(shape.name));
shape.draw();
}
}运行时包装器提供自由函数 si::is<T> 和 si::get<T>,用于精确检查具体类型。get<T> 返回引用,不转移所有权;类型不匹配时抛出 std::bad_cast。
si::existential<Drawable> shape = Circle{ "circle" };
if (si::is<Circle>(shape)) {
Circle& circle = si::get<Circle>(shape);
circle.draw();
}
const auto& const_shape = shape;
const Circle& const_circle = si::get<Circle>(const_shape);
Circle circle{ "borrowed" };
si::existential_ref<Drawable> reference = circle;
Circle& borrowed = si::get<Circle>(reference);
borrowed.name = "updated";si::existential_ref<const I> 提供只读视图,get<T> 返回 const T&。
const Circle circle{ "read-only" };
si::existential_ref<const Drawable> reference = circle;
const Circle& value = si::get<Circle>(reference);完整示例在 examples/basic.cpp。
SI 描述的是句法接口:一个类型公开提供哪些成员,而不约束这些成员的语义。
struct Drawable {
void draw() const;
};
struct Circle {
void draw() const { std::println("Circle"); }
};
static_assert(si::satisfies<Circle, Drawable>);接口定义不需要宏、不需要继承特殊基类,也不需要外部代码生成器。
si::satisfies<T, I>:检查类型T是否满足接口I。si::satisfies<I> auto:用于 abbreviated function template。si::existential<I>:拥有型、可拷贝的运行时接口对象。si::existential_move_only<I>:拥有型、仅移动的运行时接口对象。si::existential_ref<I>:非拥有引用型运行时接口对象。- 支持成员函数的名称、参数、返回值、cv/ref 限定和
noexcept精确匹配。 - 运行时接口对象通过点运算符暴露接口成员:函数可写
obj.draw(),数据成员是原始字段指针,可写*(obj.field)。 - 支持接口继承组合。
- private / protected 成员默认不匹配。
本项目使用 xmake 构建,测试框架为 boost.ut。
当前需要支持 C++26 静态反射的 GCC,并启用:
-std=c++26 -freflection
构建测试:
xmake test -v构建并运行示例:
xmake build -v structural_interface_example_basic
xmake run structural_interface_example_basic- runtime 同名 callable 成员已支持普通函数调用,但重载成员的 runtime proxy 仍需要进一步设计。
- 数据成员当前以原始字段指针形式暴露,例如
obj.name的类型为std::string*。