Home > Net >  Constexpr CRTP destructor
Constexpr CRTP destructor

Time:10-26

I created the constexpr version of the Curiously Recurring Template Pattern and all seem to work as expected, except the destructor who "under normal circumstances" should be marked as virtual. As I can understand, virtual is the vital enemy of constexpr.

In my example I implemented two interfaces with no data-members. Is it correct in the general case (with data-members) to let virtual ~Crtp() = default;, virtual ~FeatureNamesInterface() = default; and virtual ~FeatureValuesInterface() = default; commented out and let the compiler to define the destructors? Does this approach have a memory leak? Is it a better approach to make them protected? Any other solution that work with constexpr would be welcome!

The interface code look like this

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

And the implementation of the two sample classes look like this

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;
        }
    };
}

The full example can be found here.

CodePudding user response:

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

Something is iffy here. It sounds like you are operating on the assumption that "All classes should have a virtual destructor", which is incorrect.

virtual destructors are only required if there is a possibility that a derived class might be deleted from a pointer to the base. As a matter of safety, it is also generally encouraged to systematically have a virtual destructor if the base has any other virtual methods because the resulting additional overhead is negligible, since there is already a vtable present.

On the flip side, if a class has no virtual methods, then there's generally no reason to ever hold a owning pointer to a derived object in a pointer to the base. On top of that, the virtual destructor's overhead becomes proportionally a lot larger because there would be no need for a vtable at all without it. A virtual destructor becomes likely to cause more harm than good, unless you know for sure that you will need it.

It's even more clear-cut in the case of CRTP because pointers to base CRTP types are generally never a thing in the first place since:

  • They only ever have one Derived class.
  • The base class is unusable by itself on account of the casts to the Derived class.

Is it a better approach to make them protected?

That's generally a good approach for non-polymorphic classes that are meant to be inherited. It ensures that the only thing ever calling the destructor will be a derived class's destructor, which provides a hard guarantee that destruction will never need to be virtual.

In the case of CRTP, though, it almost borders on overkill. Just leaving the destructor defaulted out is generally deemed acceptable.

  • Related