C++26 反射注解:自动成员验证
在探索 C++26 系列的过程中,我之前的文章已经介绍了如何在 Ubuntu 上搭建 C++26 构建环境。之后,我探索了另一个名为模板的特性,并通过一个简单的示例讲解了反射的用法,并进一步学习了如何使用反射进行 JSON 序列化。在本文中,我将解释另一个极其强大的 C++26 特性,它叫做……注释以及它们与反思相结合如何使我们的生活更轻松。
首先,让我们来看下面这个假设的MotorConfig班级:
class MotorConfig
{
public:
MotorConfig(int rpm, double maxTemperature, std::string deviceName);
[[nodiscard]]
int rpm() const noexcept
{
return m_rpm;
}
[[nodiscard]]
double max_temperature() const noexcept
{
return m_maxTemperature;
}
[[nodiscard]]
const std::string& device_name() const noexcept
{
return m_deviceName;
}
private:
int m_rpm;
double m_maxTemperature;
std::string m_deviceName;
};这个类有一些私有成员m_rpm,m_maxTemperature这些m_deviceName成员在构造函数创建对象时会被初始化为特定的值。现在,假设你不希望类实例创建时使用超出某个范围的值,并且你想确保在构造时进行验证。你有哪些选择呢?你可以在构造函数中执行类似下面的操作:
MotorConfig::MotorConfig(int rpm,
double maxTemperature,
std::string deviceName)
: m_rpm{rpm},
m_maxTemperature{maxTemperature},
m_deviceName{std::move(deviceName)}
{
if (m_rpm < 0 || m_rpm > 10000)
{
throw std::out_of_range(
"m_rpm is outside the permitted range [0, 10000]");
}
if (m_maxTemperature < -40.0 || m_maxTemperature > 150.0)
{
throw std::out_of_range(
"m_maxTemperature is outside the permitted range [-40, 150]");
}
if (m_deviceName.empty())
{
throw std::invalid_argument(
"m_deviceName is required");
}
}太好了!但是,如果向类中添加了另一个类成员,而该成员的值也需要验证,那么您必须在构造函数中手动添加验证代码来处理新成员。手动添加很容易出错。在 C++26 中,我们可以使用注解等特性,并结合反射等特性来自动化此验证过程。
使用注释 C++26 引入了注解的概念。注解的一般语法如下:
[[=constant-expression]] declaration 这意味着您可以将常量表达式与声明关联起来,然后在编译时检索这些值。我们稍后会详细介绍。此类注解的示例如下所示:
[[=42]] int value; 我们还可以使用用户自定义数据类型来定义更个性化的注释。
注解的用户自定义类型 您还可以使用已定义的类型作为声明的注解,如下所示:
[[=AnnotationType{arguments…}]] declaration 例如,在我们的示例代码中,我们可以声明类型为 and 的注解Range,Required如下所示:
struct Range
{
double minimum;
double maximum;
};
struct Required
{
};声明完成后,我们现在可以使用用户定义的类型注解将 MotorConfig 类重写如下:
class MotorConfig
{
public:
MotorConfig(int rpm, double maxTemperature, std::string deviceName);
[[nodiscard]]
int rpm() const noexcept
{
return m_rpm;
}
[[nodiscard]]
double max_temperature() const noexcept
{
return m_maxTemperature;
}
[[nodiscard]]
const std::string& device_name() const noexcept
{
return m_deviceName;
}
private:
[[= Range{0.0, 10000.0}]]
int m_rpm;
[[= Range{-40.0, 150.0}]]
double m_maxTemperature;
[[= Required{}]]
std::string m_deviceName;
};仔细阅读声明:
[[= Range{0.0, 10000.0}]]
int m_rpm;
它表示成员变量的允许取值范围m_rpm为0.00 到10000.01。然而,目前该注解仅提供附加信息,它本身并不强制执行取值范围,除非后续代码强制执行,否则可以忽略。
现在反思一下
如上文代码所示,一旦注释就位,我们现在就可以利用反射来提取它们并在代码中使用它们。
首先,我们将编写一个循环来遍历类中的所有非静态成员MotorConfig:
```C++
template for (constexpr std::meta::info member :
std::define_static_array(
std::meta::nonstatic_data_members_of(^^T,
std::meta::access_context::unchecked())))我在本文template for中详细讨论了这一点。简而言之,它表示类型为的反射,在本例中为。它返回一个包含所有非静态成员反射的向量。但由于该向量使用动态存储,因此不能用作的范围。要将其与一起使用,我们需要将其复制到静态数组中,这可以通过来实现。^^TTMotorConfigstd::meta::nonstatic_data_members_oftemplate fortemplate forstd::define_static_array
然后,在每次迭代中,我们提取每个成员的注释:
constexpr auto ranges =
std::define_static_array(std::meta::annotations_of_with_type(
member, ^^Range));该函数std::meta::annotations_of_with_type返回一个向量,其中包含所有类型为 <input> 的注解的反射Range。std::define_static_array它将这些反射复制到一个静态存储的数组中,并返回一个std::span指向该数组的引用。<input> 通常将其元素存储在动态分配的内存中,因此其内容不能直接存储在constexpr变量中ranges。
单注与多注 请记住,每种类型中只能有一个注解:
[[= Range{0.0, 10000.0}]] int m_rpm; 但是,也可以在一个类型中包含多个注解:
[[=Range{0.0, 10000.0}, =Range{1000.0, 8000.0}]] int m_rpm; 你可以用以下任何一种方式表达:
[[=Range{0.0, 3000.0}, =Range{7000.0, 10000.0}]] int m_rpm; 或者分别如下:
[[=Range{0.0, 3000.0}]] [[=Range{7000.0, 10000.0}]] int m_rpm; 因此,在以下表达式中,ranges表示span引用注解的静态反射数组:
constexpr auto ranges = std::define_static_array(std::meta::annotations_of_with_type( member, ^^Range)); 在我们的例子中,我们只有一个注解,其反射可以用 表示ranges[0]。为了提取可用范围,我们可以这样做:
constexpr Range range = std::meta::extract [:member:]这是一个 C++26 拼接表达式。它将反射转换回该反射所代表的实体。因此,在第一次迭代中object.[:member:],它将给出 的值object.m_rpm,现在我们可以进行如下比较: if (value < range.minimum || value > range.maximum) { throw std::out_of_range(std::string{name} + “ is outside the permitted range [“ + std::to_string(range.minimum) + “, “ + std::to_string(range.maximum) + “]”); } 在下一次迭代中,它将object.[:member:]给出值,object.m_maxTemperature依此类推。以下代码片段实现了所有这些功能: 注解的处理方式Required非常相似,可以大致按如下方式实现: 施工阶段验证 验证功能在以下模板函数中实现: 完整的源代码可在 GitHub 链接中找到。要编译代码,请在命令行中执行以下操作: 为了在程序启动时就启用验证,我们可以按如下方式在类validate_or_throw的构造函数中调用验证函数模板:MotorConfig 这样可以确保当传递给构造函数的MotorConfig参数值超出允许范围时,构造函数会抛出异常。手动检查传递的参数值的麻烦就此消失了!template for (constexpr std::meta::info member : std::define_static_array(
std::meta::nonstatic_data_members_of(^^T,
std::meta::access_context::unchecked())))
{
...
...
constexpr auto ranges =
std::define_static_array(std::meta::annotations_of_with_type(member, ^^Range));
if constexpr (!ranges.empty())
{
...
const auto& value = object.[:member:];template <typename T>
void validate_or_throw(const T& object)
{
template for (constexpr std::meta::info member : std::define_static_array(
std::meta::nonstatic_data_members_of(^^T,
std::meta::access_context::unchecked())))
{
constexpr auto ranges =
std::define_static_array(std::meta::annotations_of_with_type(
member, ^^Range));
if constexpr (!ranges.empty())
{
constexpr Range range = std::meta::extract<Range>(ranges[0]);
const auto& value = object.[:member:];
if (value < range.minimum || value > range.maximum)
{
throw std::out_of_range(std::string{name}
+ " is outside the permitted range ["
+ std::to_string(range.minimum) + ", "
+ std::to_string(range.maximum) + "]");
}
}constexpr auto required =
std::define_static_array(std::meta::annotations_of_with_type(member, ^^Required));
if constexpr (!required.empty())
{
const auto& value = object.[:member:];
if constexpr (requires { value.empty(); })
{
if (value.empty())
{
throw std::invalid_argument(std::string{name} + " is required");
}
}
else
{
static_assert(
requires { value.empty(); },
"Required annotation can only be applied to a type "
"that provides empty()");
}
}template <typename T>
void validate_or_throw(const T& object)
{
template for (constexpr std::meta::info member : std::define_static_array(
std::meta::nonstatic_data_members_of(^^T,
std::meta::access_context::unchecked())))
{
constexpr std::string_view name = std::meta::identifier_of(member);
std::cout << "Validating member: " << name << '\n';
constexpr auto ranges =
std::define_static_array(std::meta::annotations_of_with_type(member, ^^Range));
constexpr auto range_count = ranges.size();
std::cout << "Found " << range_count << " range annotations for member: " << name << '\n';
if constexpr (!ranges.empty())
{
constexpr Range range = std::meta::extract<Range>(ranges[0]);
const auto& value = object.[:member:];
if (value < range.minimum || value > range.maximum)
{
throw std::out_of_range(std::string{name} + " is outside the permitted range ["
+ std::to_string(range.minimum) + ", "
+ std::to_string(range.maximum) + "]");
}
}
constexpr auto required =
std::define_static_array(std::meta::annotations_of_with_type(member, ^^Required));
if constexpr (!required.empty())
{
const auto& value = object.[:member:];
if constexpr (requires { value.empty(); })
{
if (value.empty())
{
throw std::invalid_argument(std::string{name} + " is required");
}
}
else
{
static_assert(
requires { value.empty(); },
"Required annotation can only be applied to a type "
"that provides empty()");
}
}
}
}g++ -std=c++26 \
-freflection \
-Wall \
-Wextra \
-Wpedantic \
reflection_validation.cpp \
-o reflection_validation
./reflection_validation
Validating member: m_rpm
Found 1 range annotations for member: m_rpm
Construction failed: m_rpm is outside the permitted range [0, 10000]
Validating member: m_rpm
Found 1 range annotations for member: m_rpm
Validating member: m_maxTemperature
Found 1 range annotations for member: m_maxTemperature
Validating member: m_deviceName
Found 0 range annotations for member: m_deviceName
Valid configuration created.
RPM: 5000
Maximum temperature: 85
Device name: motor-controller-1MotorConfig::MotorConfig(int rpm, double maxTemperature, std::string deviceName)
: m_rpm{rpm},
m_maxTemperature{maxTemperature},
m_deviceName{std::move(deviceName)}
{
validate_or_throw(*this);
}