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.
Thankfully, with C++20 constraints implementing this almost results in the pseudocode of the original question that is both easy to understand and implement:
(godbolt link here)
The appropriate destructor is selected using overload resolution (C++20 standard §11.4.7.4 [class.dtor]):
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]):
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.