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.

Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
  • 1
    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...` ). – Nawaz Jun 17 '15 at 05:28
  • 1
    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`'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. – Tony Delroy Jun 17 '15 at 05:35
  • @Nawaz The scenario arises when we need to define *conditionally literal* class. I provide the complete example and one possible flawed solution. – Tomilov Anatoliy Jun 17 '15 at 05:40
  • @TonyD Don't look at `X`, but at `U`. In *C++* `union`s can't have a base classes of to be a base classes. – Tomilov Anatoliy Jun 17 '15 at 05:42
  • @Orient: Define the non-trivial destructor. At least we would know what you want to do there. – Nawaz Jun 17 '15 at 05:42
  • @Nawaz Are you understand the example with `U`? It is evident, that the problem really exist. – Tomilov Anatoliy Jun 17 '15 at 05:45
  • @Orient: I'm looking for alternative solution. Please do post the destructor implementation here. That is *more* relevant here. – Nawaz Jun 17 '15 at 05:46
  • @Nawaz `Define the non-trivial destructor`: already done. See the code of `U`. – Tomilov Anatoliy Jun 17 '15 at 05:46
  • @Nawaz Personally for you I made a quoting: `~U() { if (active()) { head_.~head(); } else { tail_.~tail(); } }` – Tomilov Anatoliy Jun 17 '15 at 05:48
  • @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? – Nawaz Jun 17 '15 at 05:50
  • 1
    @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. – Tomilov Anatoliy Jun 17 '15 at 05:51
  • @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. – Nawaz Jun 17 '15 at 05:57
  • ... something like : `~U() { destructor{} && ..>::destruct(head_, tail_); }` – Nawaz Jun 17 '15 at 05:59
  • @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. – Tomilov Anatoliy Jun 17 '15 at 06:00
  • If you want to make it `constexpr` then why would you use `U` in the first place? Why not use `int` directly? – Nawaz Jun 17 '15 at 06:01
  • 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` that contains a `union { Head h; V tail; };` rather than a plain union. – T.C. Jun 17 '15 at 06:01
  • @T.C. very interesting! I will try. – Tomilov Anatoliy Jun 17 '15 at 06:04
  • @T.C. Is it still permittable for common initial sequences to be accessible from non-active member as pointed [here](http://talesofcpp.fusionfenix.com/post-20/eggs.variant---part-ii-the-constexpr-experience#a-note-on-standard-layout-unions)? – Tomilov Anatoliy Jun 17 '15 at 06:08
  • 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`) contains a standard-layout struct `indexed` and a standard-layout union `storage`; 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. – T.C. Jun 17 '15 at 06:17
  • @T.C. Hence, my implementation is incorrect (UB?)? – Tomilov Anatoliy Jun 17 '15 at 06:21
  • Alas `union { Head h; V tail; };` gives nothing. – Tomilov Anatoliy Jun 17 '15 at 06:26
  • I just wanted to throw this in for reference, maybe it helps you: https://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? – Daniel Jour Jun 17 '15 at 07:13
  • Please see http://talesofcpp.fusionfenix.com/post-20/eggs.variant---part-ii-the-constexpr-experience which tries to solve the same problem. – dyp Jun 17 '15 at 07:57
  • @dyp Writing my code is already inspired by linked work. – Tomilov Anatoliy Jun 17 '15 at 08:05
  • @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](https://github.com/tomilov/versatile) (`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.](https://github.com/tomilov/insituc/blob/master/include/insituc/ast/ast.hpp) – Tomilov Anatoliy Jun 17 '15 at 12:52
  • 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. – Yakk - Adam Nevraumont Jun 17 '15 at 13:15
  • @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. – Tomilov Anatoliy Jun 17 '15 at 13:48
  • 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? – Yakk - Adam Nevraumont Jun 17 '15 at 14:03
  • @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`](http://en.cppreference.com/w/cpp/types/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). – Tomilov Anatoliy Jun 17 '15 at 16:22
  • 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. – T.C. Jun 17 '15 at 22:01
  • in [C++20], the answer will be simplified using `requires` clause but this question is tagged [C++11]/[C++14] only. – Desmond Gold Feb 09 '22 at 02:06

2 Answers2

3

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;
}
Evgeny Panasyuk
  • 9,076
  • 1
  • 33
  • 54
  • 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. – Tomilov Anatoliy Jul 01 '15 at 18:32
  • 1
    @Orient Class-level `static if` may improve syntax. Andrei Alexandrescu discusses similar situation at his talk ["Static If I Had a Hammer"](https://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Static-If-I-Had-a-Hammer). There are some `static if` proposals to ISO C++ - for example [N3329](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3329.pdf). – Evgeny Panasyuk Jul 01 '15 at 18:40
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.

Rane
  • 321
  • 2
  • 6