【问题标题】:Constexpr CRTP destructorConsexpr CRTP 析构函数
【发布时间】:2021-12-11 05:28:34
【问题描述】:

我创建了 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找到。

【问题讨论】:

  • 请注意,非 const child() 也可能是 constexpr
  • CRTP 与动态多态性相反的重点是避免虚函数,包括 dtor。

标签: c++ destructor compile-time crtp


【解决方案1】:

除了“在正常情况下”应该被标记为虚拟的析构函数

这里有些东西是不确定的。听起来您是在假设“所有类都应该有一个虚拟析构函数”的情况下进行操作,这是不正确的。

virtual 仅当派生类可能从指向基的指针中删除时才需要析构函数。为了安全起见,如果基础有任何其他虚拟方法,通常也鼓励系统地使用虚拟析构函数,因为由此产生的额外开销可以忽略不计,因为已经存在 vtable。 p>

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

在 CRTP 的情况下更加明确,因为指向基本 CRTP 类型的指针通常从一开始就不是一个东西,因为:

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

保护它们是更好的方法吗?

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

但是,在 CRTP 的情况下,它几乎接近于矫枉过正。只是让析构函数默认退出通常被认为是可以接受的。

【讨论】:

  • 另外提供受保护的默认析构函数将删除默认的移动构造函数和赋值运算符,然后我必须重新启用它们。所以是的,这是一个矫枉过正!谢谢你的回答!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-24
  • 2019-01-23
  • 1970-01-01
  • 2014-03-23
  • 2023-04-07
相关资源
最近更新 更多