Engineerist Engineerist - 2 months ago 16
C++ Question

SFINAE conditional and constructor argument types

I have encountered the following technique that allows the construction of a wrapper object around

T
, but from objects of type
U
, if
T
is constructible from
U
:

template< typename T >
struct S {
template< typename U,
typename = std::enable_if_t<
std::is_constructible< T, U >::value > >
explicit S (U&& arg) : value (arg)
{ }
...
};


IIUC, the type
U
used in the
is_constructible
test can be different from the cv-qualified type of
arg
.

Is it possible that the SFINAE test could fail although the expression
value(arg)
is valid?

Answer

Is it possible that the SFINAE test could fail although the expression value(arg) is valid?

The way you wrote S: yes, it's possible.
It follows a minimal, (not) working example:

#include<type_traits>

template< typename T >
struct S {
    template< typename U
              , typename = std::enable_if_t<std::is_constructible< T, U >::value >
    > explicit S (U&& arg) : value{arg} {}

    T value;
};

struct A {};
struct B {
    B(A &) {}
};

int main() {
    S<B> s(A{});
}

The code above doesn't compile, but it does compile if you comment out the following line:

, typename = std::enable_if_t<std::is_constructible< T, U >::value >

The problem is that you are not forwarding the constructor parameter. Instead, you are using an lvalue reference to a variable of type rvalue reference to A (that is arg) as an argument for value.
B is not constructible from an rvalue reference to A and the sfinae expression fails (correctly), but you are not actually using such a reference to construct the parameter, so removing the sfinae expression it works.
In fact, B is constructible from an lvalue reference to A, that is what you are using when you write value{arg}.


You should rather write S as it follows:

#include<utility>

// ...

template< typename T >
struct S {
    template< typename U
              , typename = std::enable_if_t<std::is_constructible< T, U >::value >
    > explicit S (U&& arg) : value (std::forward<U>(arg)) { }

    T value;
};

Note the use of std::forward to actually forward the parameters with the right types.
This at least solves the above mentioned issue and it should give you the guarantees you were looking for.

Comments