There's a hole in our template-metaprogramming facilities. We can figure out at compile time the arguments of any function by name…except for those of a constructor. This seemingly innocuous limitation has a drastic negative impact on std::variant
in that a std::variant<int, float>
can surprisingly get into the "corrupted_by_exception" state. There are likely several other instances where this hole causes problems. This paper proposes a modest new standard library type, std::direct_init
that removes the hole and solves the variant problem.
The expectation is that it is impossible to get a std::variant
with "friendly" alternative types, such as int
and float
, into the "corrupted_by_exception" state. On the LEWG reflector, however, Augustín K-ballo Bergé demonstrated that a specially constructed class can put any variant into the “corrupted_by_exception” state.
Consider the following code:
struct nasty { operator int() const { throw 42; } };
// elsewhere…
variant<int, float> v;
v.emplace<int>(nasty{});
Note carefully, that inside emplace this does something like
destroy_old_value();
new (storage) int(std::forward(arg));
ie.
destroy_old_value();
new (storage) int(nasty_arg)
And the conversion from nasty
to int
happens inside the emplace function after the old value has been destroyed.
We really want emplace
to construct in place, not in a temporary followed by a move (otherwise things like std::mutex
won't work). So we can't create a temporary first. What we would like to do is make temporaries of the args:
int && arg0 = int(std::forward(args)); // might throw here
destroy_old_value();
new (storage) int(arg0);
To do this, we need to know which constructor would be called, and be able to make temporary refs of all the args casted into the constructor args. Unfortunately, there doesn't seem to be a way to do this with C++.
std::direct_init<T>
represents the direct initialization of a T
object. For every constructor and constructor template in T
, std::direct_init<T>
has a corresponding call operator. The signature of the call operator mimics that of its corresponding constructor while its return type is always T
. The semantics of the call operator is that it will return a T
object that is initialized by the corresponding constructor.
If T
is a non-class type we still want std::direct_init<T>
to model initialization. In the T(arg)
case std::direct_init<T>
’s call operator will have a non-reference parameter type if the cast performs an lvalue-to-rvalue conversion, and otherwise has the appropriate reference type.
Library Fundamentals Version 2 (N4564) provides std::raw_invocation_type
which can be used to inspect the parameter types of an invokable function given a sequence of argument types. This tool, when combined with std::direct_init<T>
, allows std::variant
’s emplace
implementation to convert emplace
’s arguments into the appropriate alternative’s parameter types before the constructor is actually called. Consider the following sketch implementation of emplace
.
template<typename T> struct nondeduced { using type = T; };
template<typename T> using nondeduced_t = nondeduced<T>::type;
template<typename X, typename ...T> void variant<...>::emplace_impl(X (*)(T...), nondeduced_t<T> ...t) {
static_assert(noexcept(X(std::forward<T>(t)...)), "need noexcept constructors"); // or switch to a different technique or whatever here
call_dtor(kind, storage);
kind = kindof<X>;
new (storage) X(std::forward<T>(t)...);
}
template<typename X, typename ...T> void variant<...>::emplace(T &&...t) {
emplace_impl((std::raw_invocation_type_t<direct_init<X>(decltype(t)...)>*)nullptr, std::forward<T>(t)...);
}