Conditionally trivial destructor

2.2k views Asked by At

Inventing a discriminated union/tagged variant I conclude that there is particular need in such a feature as "make destructor trivial on some conditions at compile time". I mean some kind of SFINAE or something like (pseudocode):

template< typename ...types >
struct X
{
    ~X() = default((std::is_trivially_destructible< types >{} && ...))
    {
        // non-trivial code here
    }
};

Which means that if condition in default(*) is true, then definition of destructor is equal to ~X() = default;, but if it is false then { // ... } body used instead.

#pragma once
#include <type_traits>
#include <utility>
#include <experimental/optional>

#include <cassert>

template< typename ...types >
class U;

template<>
class U<>
{

    U() = delete;

    U(U &) = delete;
    U(U const &) = delete;
    U(U &&) = delete;
    U(U const &&) = delete;

    void operator = (U &) = delete;
    void operator = (U const &) = delete;
    void operator = (U &&) = delete;
    void operator = (U const &&) = delete;

};

template< typename first, typename ...rest >
class U< first, rest... >
{

    struct head
    {

        std::size_t which_;
        first value_;

        template< typename ...types >
        constexpr
        head(std::experimental::in_place_t, types &&... _values)
            : which_{sizeof...(rest)}
            , value_(std::forward< types >(_values)...)
        { ; }

        template< typename type >
        constexpr
        head(type && _value)
            : head(std::experimental::in_place, std::forward< type >(_value))
        { ; }

    };

    using tail = U< rest... >;

    union
    {

        head head_;
        tail tail_;

    };

    template< typename ...types >
    constexpr
    U(std::true_type, types &&... _values)
        : head_(std::forward< types >(_values)...)
    { ; }

    template< typename ...types >
    constexpr
    U(std::false_type, types &&... _values)
        : tail_(std::forward< types >(_values)...)
    { ; }

public :

    using this_type = first; // place for recursive_wrapper filtering

    constexpr
    std::size_t
    which() const
    {
        return head_.which_;
    }

    constexpr
    U()
        : U(typename std::is_default_constructible< this_type >::type{}, std::experimental::in_place)
    { ; }

    U(U &) = delete;
    U(U const &) = delete;
    U(U &&) = delete;
    U(U const &&) = delete;

    template< typename type >
    constexpr
    U(type && _value)
        : U(typename std::is_same< this_type, std::decay_t< type > >::type{}, std::forward< type >(_value))
    { ; }

    template< typename ...types >
    constexpr
    U(std::experimental::in_place_t, types &&... _values)
        : U(typename std::is_constructible< this_type, types... >::type{}, std::experimental::in_place, std::forward< types >(_values)...)
    { ; }

    void operator = (U &) = delete;
    void operator = (U const &) = delete;
    void operator = (U &&) = delete;
    void operator = (U const &&) = delete;

    template< typename type >
    constexpr
    void
    operator = (type && _value) &
    {
        operator std::decay_t< type > & () = std::forward< type >(_value);
    }

    constexpr
    explicit
    operator this_type & () &
    {
        assert(sizeof...(rest) == which());
        return head_.value_;
    }

    constexpr
    explicit
    operator this_type const & () const &
    {
        assert(sizeof...(rest) == which());
        return head_.value_;
    }

    constexpr
    explicit
    operator this_type && () &&
    {
        assert(sizeof...(rest) == which());
        return std::move(head_.value_);
    }

    constexpr
    explicit
    operator this_type const && () const &&
    {
        assert(sizeof...(rest) == which());
        return std::move(head_.value_);
    }

    template< typename type >
    constexpr
    explicit
    operator type & () &
    {
        return static_cast< type & >(tail_);
    }

    template< typename type >
    constexpr
    explicit
    operator type const & () const &
    {
        return static_cast< type const & >(tail_);
    }

    template< typename type >
    constexpr
    explicit
    operator type && () &&
    { 
        //return static_cast< type && >(std::move(tail_)); // There is known clang++ bug #19917 for static_cast to rvalue reference.
        return static_cast< type && >(static_cast< type & >(tail_)); // workaround
    }

    template< typename type >
    constexpr
    explicit
    operator type const && () const &&
    {
        //return static_cast< type const && >(std::move(tail_));
        return static_cast< type const && >(static_cast< type const & >(tail_));
    }

    ~U()
    {
        if (which() == sizeof...(rest)) {
            head_.~head();
        } else {
            tail_.~tail();
        }
    }

};

// main.cpp
#include <cstdlib>

int
main()
{
    U< int, double > u{1.0};
    assert(static_cast< double >(u) == 1.0);
    u = 0.0;
    assert(static_cast< double >(u) == 0.0);
    U< int, double > w{1};
    assert(static_cast< int >(w) == 1);
    return EXIT_SUCCESS;
}

In this example for making the class U a literal type (in case of first, rest... are all the trivially destructible) it is possible to define almost the same as U class (V), but without definition of a destructor ~U (i.e. is literal type if all descending types are literals). Then define template type alias

template< typename ...types >
using W = std::conditional_t< (std::is_trivially_destructible< types >{} && ...), V< types... >, U< types... > >;

and redefine using tail = W< rest... >; in both U and V. Therefore, there are two almost identical classes, differs only in presence of destructor. Above approach requires excessive duplication of code.

The problem also concerned with trivially copy/move assignable types and operator = and also all other conditions for type to be std::is_trivially_copyable. 5 conditions gives totally a 2^5 combinations to implement.

Is there any ready to use technique (and less verbose, then described above one) expressible in present C++ I miss, or maybe coming soon proposal ?

Another thinkable approach is (language feature) to mark the destructor as constexpr and grant to the compiler to test whether the body is equivalent to trivial one during instantiation or not.

UPDATE:

Code simplified as pointed out in comments: union became union-like class. Removed noexcept specifiers.

2

There are 2 answers

0
Rane On BEST ANSWER

Thankfully, with C++20 constraints implementing this almost results in the pseudocode of the original question that is both easy to understand and implement:

#include <type_traits>
#include <optional>
#include <string>
#include <vector>

template< typename ...types >
struct X
{
    ~X() = default;
    
    ~X() requires (!(std::is_trivially_destructible_v<types> && ...))
    {
    }
};

int main()
{
    static_assert(std::is_trivially_destructible_v<
        X<>
    >);
    static_assert(std::is_trivially_destructible_v<
        X<float, int, char>
    >);
    static_assert(!std::is_trivially_destructible_v<
        X<std::vector<int>, std::vector<char>>
    >);
    static_assert(!std::is_trivially_destructible_v<
        X<std::string, int, float>
    >);
    static_assert(std::is_trivially_destructible_v<
        X<std::optional<int>, int, float>
    >);
}

(godbolt link here)

The appropriate destructor is selected using overload resolution (C++20 standard §11.4.7.4 [class.dtor]):

At the end of the definition of a class, overload resolution is performed among the prospective destructors declared in that class with an empty argument list to select the destructor for the class, also known as the selected destructor. The program is ill-formed if overload resolution fails. Destructor selection does not constitute a reference to, or odr-use ([basic.def.odr]) of, the selected destructor, and in particular, the selected destructor may be deleted ([dcl.fct.def.delete]).

The entire C++'s overload resolution is rather long and complicated in stardardese, but briefly put, the overload resolution chooses the destructor that satisfies the constraints and is the most constrained (C++20 standard §12.2.3.1 [over.match.viable]):

From the set of candidate functions constructed for a given context ([over.match.funcs]), a set of viable functions is chosen, from which the best function will be selected by comparing argument conversion sequences and associated constraints ([temp.constr.decl]) for the best fit ([over.match.best]). The selection of viable functions considers associated constraints, if any, and relationships between arguments and function parameters other than the ranking of conversion sequences.

Note that this strategy can be applied to other special member functions as well (constructors, assignment operators etc.). Although the P0848R3 - Conditionally Trivial Special Member Functions proposal is only partially implemented in the recent clang 16 release, while gcc >= 10 and MSVC >= VS 2019 16.8 are fully conformant.

2
Evgeny Panasyuk On

Conditional destructor can be implemented via additional intermediate layer with template specialization. For example:

Live Demo on Coliru

#include <type_traits>
#include <iostream>
#include <vector>

using namespace std;

template<typename T>
class storage
{
    aligned_storage_t<sizeof(T)> buf;

    storage(storage&&) = delete;
public:
    storage()
    {
        new (&buf) T{};
    }
    T &operator*()
    {
        return *static_cast<T*>(&buf);
    }
    void destroy()
    {
        (**this).~T();
    }
};

template<typename T, bool destructor>
struct conditional_storage_destructor 
{
    storage<T> x;
};

template<typename T>
struct conditional_storage_destructor<T, true> : protected storage<T>
{
    storage<T> x;

    ~conditional_storage_destructor()
    {
        x.destroy();
    }
};

template<typename T>
class wrapper
{
    conditional_storage_destructor<T, not is_trivially_destructible<T>::value> x;
public:
    T &operator*()
    {
        return *(x.x);
    }
};

int main()
{
    static_assert(is_trivially_destructible< wrapper<int> >::value);
    static_assert(not is_trivially_destructible< wrapper<vector<int>> >::value);

    cout << "executed" << endl;
}