Conditionally trivial destructor
Asked Answered
M

2

11

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.

Midgut answered 17/6, 2015 at 5:9 Comment(30)
I dont know the scenario which could lead to such a design of destructor. I also feel that whatever be that scenario, that could be solved with proper use of RAII (implemented by the template arguments types... ).Newel
I don't see why you want/need X to store the values and manage destruction directly. Perhaps have a member or base templated on the std::is_trivially_destructible value, then you can specialise Member_Or_Base<false>'s destructor to preform desired destruction. And that's way too much code to post - when you have a specific issue create minimal code illustrating it.Condescend
@Nawaz The scenario arises when we need to define conditionally literal class. I provide the complete example and one possible flawed solution.Midgut
@TonyD Don't look at X, but at U. In C++ unions can't have a base classes of to be a base classes.Midgut
@Orient: Define the non-trivial destructor. At least we would know what you want to do there.Newel
@Nawaz Are you understand the example with U? It is evident, that the problem really exist.Midgut
@Orient: I'm looking for alternative solution. Please do post the destructor implementation here. That is more relevant here.Newel
@Nawaz Define the non-trivial destructor: already done. See the code of U.Midgut
@Nawaz Personally for you I made a quoting: ~U() { if (active()) { head_.~head(); } else { tail_.~tail(); } }Midgut
@Orient: Ohh. I see.. Do you realize non-pointer object will be destroyed by invoking the destructor, anyway? So what is the point of writing head_.~head() when it will be invoked anyway because head_ is non-pointer and the object which is holding it is getting destroyed?Newel
@Nawaz Do you realize U is union, but not struct or class? You must provide a destructor if any of underlying types is non-trivially destructible.Midgut
@Orient: Not till now. Your code is too long for it. Now I see the problem. But still I dont see why you want to define the destructor conditionally. You can use the triviality of types in the destructor itself, and let it be non-trivial permanently.Newel
... something like : ~U() { destructor<is_trivially_destructible<types>{} && ..>::destruct(head_, tail_); }Newel
@Nawaz In such a case I can't declare constexpr U< int, char > u{1}; <=> due to non-triviality of destructor U is not literal.Midgut
If you want to make it constexpr then why would you use U in the first place? Why not use int directly?Newel
What @TonyD outlined is pretty much the only way to do this at present. To minimize the code duplication, factor out the storage part as much as you can. You may need a struct V<Head, Tail...> that contains a union { Head h; V<Tail...> tail; }; rather than a plain union.Mcauley
@Mcauley Is it still permittable for common initial sequences to be accessible from non-active member as pointed here?Midgut
I'm doubtful, and I think the reading of the standard on that page is actually incorrect. The standard-layout union on that page (storage<I, T, Ts...>) contains a standard-layout struct indexed<T> and a standard-layout union storage<I+1, Ts...>; it doesn't contain "several standard-layout structs" - only one. As far as I can tell, the standard doesn't provide for recursing into union members of unions for this purpose.Mcauley
@Mcauley Hence, my implementation is incorrect (UB?)?Midgut
Alas union { Head h; V<Tail...> tail; }; gives nothing.Midgut
I just wanted to throw this in for reference, maybe it helps you: github.com/beark/ftl/blob/master/include/ftl/sum_type.h Though I still don't really understand what you want that destructor to do or why you want it. Can you give a stripped down version explaining exactly the behavior you are trying to achieve?Burns
Please see talesofcpp.fusionfenix.com/post-20/… which tries to solve the same problem.Snyder
@Snyder Writing my code is already inspired by linked work.Midgut
@Nawaz I not use int directly, because there is an idiom to implement. It make a big sense and matters much in fairly wide variety of applications. You can see full code (U is versatile here) of variant, which uses U as a storage for values of bounded types. The example of use is recursive structures like AST.Midgut
An amusing hack might me to put the operator= if non-trivial in a crtp base, and conditionally have operator= in the child type either be non-trivial call-crtp, or be assignment from a non-reachable type. I don;t think that will block operator= creation, but not certain.Admiralty
@Yakk Neither base classes nor derivation from unions are not permitted in present C++. Anyways there too many mixins should be involved to completely resolve all the possible cases.Midgut
Oh, I figure you'd wrap the union in a struct, no? Or does your technique require overloading operations within the union, with no way around it?Admiralty
@Yakk Wrapping gives nothing. This becomes clear when you try. Wrapping into struct not deny the nessesity of defining (or defaulting) destructors, copy/move assignment operators, copy/move constructors for underlying union. Looking at std::is_trivially_copyable class requirements tell me there are at least 2^5 = 32 possible combinations of "triviallity" of member functions (even avoiding const &&, &, volatile cases for c-tor and assignment operator).Midgut
You are complaining that you can't use a base class or derive from a union. And a struct wrapping a union solves that problem. There's no point in that fine-grained level of trivialness. The crucial things are trivially copyable and trivially destructible; one impacts optimizations and the other impacts literalness. For variants in particular, an assignment may often need to destroy then construct (if the stored types are different), so it's impossible to perfectly mirror the trivialness of the assignment operators in any event.Mcauley
in [C++20], the answer will be simplified using requires clause but this question is tagged [C++11]/[C++14] only.Glomerulonephritis
T
2

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.

Terrijo answered 11/4, 2023 at 18:11 Comment(0)
I
4

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;
}
Immigration answered 1/7, 2015 at 16:57 Comment(3)
I know it. But it looks like something alien and redundant. Anyways your example along with union-like classes can solve the problem completely. Alas, in very verbose way.Midgut
@Orient Class-level static if may improve syntax. Andrei Alexandrescu discusses similar situation at his talk "Static If I Had a Hammer". There are some static if proposals to ISO C++ - for example N3329.Immigration
Can C++17 (not C++20 nor C++23) simplify this?Denbrook
T
2

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.

Terrijo answered 11/4, 2023 at 18:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.