Constexpr CRTP析构函数

Constexpr CRTP destructor

我创建了 Curiously Recurring Template Pattern 的 constexpr 版本,并且一切似乎都按预期工作,除了“在正常情况下”应标记为 virtual 的析构函数。据我了解,virtualconstexpr的死敌。

在我的示例中,我实现了两个没有数据成员的接口。在一般情况下(使用数据成员)让 virtual ~Crtp() = default;virtual ~FeatureNamesInterface() = default;virtual ~FeatureValuesInterface() = default; 注释掉并让编译器定义析构函数是否正确?这种方法有内存泄漏吗?保护它们是更好的方法吗?欢迎使用 constexpr 的任何其他解决方案!

界面代码是这样的

namespace lib
{
    template <typename Derived, template<typename> class CrtpType>
    struct Crtp
    {
        //virtual ~Crtp() = default;
        [[nodiscard]] Derived& child() noexcept { return static_cast<Derived&>(*this); }
        [[nodiscard]] constexpr Derived const& child() const noexcept { return static_cast<const Derived&>(*this); }
    private:
        constexpr Crtp() = default;
        friend CrtpType<Derived>;
    };

    template<typename Derived>
    struct FeatureNamesInterface : Crtp<Derived, FeatureNamesInterface>
    {
        constexpr FeatureNamesInterface() = default;
        //virtual ~FeatureNamesInterface() = default;
        [[nodiscard]] constexpr auto& GetFeatureNames() const noexcept { return Crtp<Derived, FeatureNamesInterface>::child().GetNames(); }
    };

    template<typename Derived>
    struct FeatureDataInterface : Crtp<Derived, FeatureDataInterface>
    {
        constexpr FeatureDataInterface() = default;
        //virtual ~FeatureValuesInterface() = default;
        [[nodiscard]] constexpr auto GetFeatureData() const { return Crtp<Derived, FeatureDataInterface>::child()(); }
    };
}

两个示例的实现 类 如下所示

namespace impl
{
    class ChildOne final : public lib::FeatureNamesInterface<ChildOne>, public lib::FeatureDataInterface<ChildOne>
    {
        static constexpr std::array mNames{"X"sv, "Y"sv, "Z"sv};
    public:
        constexpr ChildOne() : FeatureNamesInterface(), FeatureDataInterface() {}
        ~ChildOne() = default;
        
        [[nodiscard]] constexpr auto& GetNames() const noexcept { return mNames; }
        
        [[nodiscard]] constexpr auto operator()() const noexcept
        {
            std::array<std::pair<std::string_view, double>, mNames.size()> data;
            double value = 1.0;
            for (std::size_t i = 0; const auto& name : mNames)
                data[i++] = {name, value++};

            return data;
        }
    };

    class ChildTwo final : public lib::FeatureNamesInterface<ChildTwo>, public lib::FeatureDataInterface<ChildTwo>
    {
        static constexpr std::array mNames{"A"sv, "B"sv, "C"sv, "D"sv, "E"sv, "F"sv};
    public:
        constexpr ChildTwo() : FeatureNamesInterface(), FeatureDataInterface() {}
        ~ChildTwo() = default;
        
        [[nodiscard]] constexpr auto& GetNames() const noexcept { return mNames; }

        [[nodiscard]] constexpr auto operator()() const noexcept
        {
            std::array<std::pair<std::string_view, double>, mNames.size()> data;
            double value = 4.0;
            for (std::size_t i = 0; const auto& name : mNames)
                data[i++] = {name, value++};

            return data;
        }
    };
}

可以找到完整的示例 here

except the destructor who "under normal circumstances" should be marked as virtual

这里有些问题。听起来你是在假设“所有 classes 都应该有一个虚拟析构函数”,这是不正确的。

virtual 析构函数仅在派生的 class 有可能从指向基的指针中删除时才需要。出于安全考虑,如果基类有任何其他虚拟方法,通常 鼓励 系统地拥有一个虚拟析构函数,因为由此产生的额外开销可以忽略不计,因为已经存在一个 vtable .

另一方面,如果 class 没有虚方法,那么通常没有理由在指向基类的指针中保存指向派生对象的拥有指针。最重要的是,虚拟析构函数的开销成比例地变得更大,因为如果没有它就根本不需要 vtable。除非您确定需要它,否则虚拟析构函数可能弊大于利。

在 CRTP 的情况下更加明确,因为指向基本 CRTP 类型的指针通常从来都不是一回事,因为:

  • 他们只有一个 Derived class。
  • 由于对 Derived class 的强制转换,基础 class 本身无法使用。

Is it a better approach to make them protected?

对于旨在被继承的非多态 classes,这通常是一个很好的方法。它确保唯一调用析构函数的是派生的 class 的析构函数,这提供了一个硬性保证,即销毁永远不需要是虚拟的。

不过,就 CRTP 而言,它几乎近乎矫枉过正。将析构函数默认设置为默认值通常被认为是可以接受的。