Skip to Main Content
C++26 反射注解:自动成员验证Back to Top

C++26 反射注解:自动成员验证

5 minutes

在探索 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(ranges[0]); 剪接表达 我们即将能够访问每个成员的当前值并将其与允许的范围进行比较。可以使用以下拼接表达式访问成员值:

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:];

[: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依此类推。以下代码片段实现了所有这些功能:

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) + "]");
            }
        }

注解的处理方式Required非常相似,可以大致按如下方式实现:

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()");
            }
        }
    }
}

完整的源代码可在 GitHub 链接中找到。要编译代码,请在命令行中执行以下操作:

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-1

为了在程序启动时就启用验证,我们可以按如下方式在类validate_or_throw的构造函数中调用验证函数模板:MotorConfig

MotorConfig::MotorConfig(int rpm, double maxTemperature, std::string deviceName)
    : m_rpm{rpm},
      m_maxTemperature{maxTemperature},
      m_deviceName{std::move(deviceName)}
{
    validate_or_throw(*this);
}

这样可以确保当传递给构造函数的MotorConfig参数值超出允许范围时,构造函数会抛出异常。手动检查传递的参数值的麻烦就此消失了!