Engineerist
Engineerist

Reputation: 367

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?

Upvotes: 4

Views: 132

Answers (1)

skypjack
skypjack

Reputation: 50540

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.

Upvotes: 4

Related Questions