Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why 'is_convertible' here in <utility> std::pair (STL)?

    template<class _Other1,
    class _Other2,
    class = enable_if_t<is_constructible<_Ty1, _Other1>::value
                    && is_constructible<_Ty2, _Other2>::value>,
    enable_if_t<is_convertible<_Other1, _Ty1>::value
            && is_convertible<_Other2, _Ty2>::value, int> = 0>
    constexpr pair(pair<_Other1, _Other2>&& _Right)
        _NOEXCEPT_OP((is_nothrow_constructible<_Ty1, _Other1>::value
            && is_nothrow_constructible<_Ty2, _Other2>::value))
    : first(_STD forward<_Other1>(_Right.first)),
        second(_STD forward<_Other2>(_Right.second))
    {   // construct from moved compatible pair
    }

template<class _Other1,
    class _Other2,
    class = enable_if_t<is_constructible<_Ty1, _Other1>::value
                    && is_constructible<_Ty2, _Other2>::value>,
    enable_if_t<!is_convertible<_Other1, _Ty1>::value
            || !is_convertible<_Other2, _Ty2>::value, int> = 0>
    constexpr explicit pair(pair<_Other1, _Other2>&& _Right)
        _NOEXCEPT_OP((is_nothrow_constructible<_Ty1, _Other1>::value
            && is_nothrow_constructible<_Ty2, _Other2>::value))
    : first(_STD forward<_Other1>(_Right.first)),
        second(_STD forward<_Other2>(_Right.second))
    {   // construct from moved compatible pair
    }

utility file for VS 2017 line 206, _Other1 and _Other2 are parameters, this is std::pair's construction func, and we are using Other1 and Other2 to initialize "first" and "second",

I think is_constructible is enough, why are we using is_convertible here?
and by the way, what's the difference between class = enable_if_t< ... ::value> and enable_if_t< ... ::value,int> = 0?

like image 840
Alvazu Avatar asked Sep 18 '17 06:09

Alvazu


People also ask

What is std:: pair used for?

std::pair is a class template that provides a way to store two heterogeneous objects as a single unit. A pair is a specific case of a std::tuple with two elements.

When should I use pair C++?

Pair is used to combine together two values that may be of different data types. Pair provides a way to store two heterogeneous objects as a single unit. It is basically used if we want to store tuples.

What is std :: Make_pair?

std::make_pairCreates a std::pair object, deducing the target type from the types of arguments.

How do you know if a pair is empty?

The default constructor of std::pair would value-initialize both elements of the pair, that means for pair<int, int> res; , its first and second would be initialized to 0 . That's the only way you can check for a default-constructed std::pair , if they're guaranteed to be non-zero after the assignment.


1 Answers

I think is_constructible is enough, why are we using is_convertible here?

The goal here is to properly handle explicit construction. Consider just doing the former and trying to write a wrapper (using REQUIRES here to hide whatever approach to SFINAE you want):

template <class T>
class wrapper {
public:
    template <class U, REQUIRES(std::is_constructible<T, U&&>::value)>
    wrapper(U&& u) : val(std::forward<U>(u)) { }
private:
    T val;
};

If that's all we had, then:

struct Imp { Imp(int ); };
struct Exp { explicit Exp(int ); };

Imp i = 0; // ok
Exp e = 0; // error
wrapper<Imp> wi = 0; // ok
wrapper<Exp> we = 0; // ok?!?

We definitely don't want that last one to be okay - that breaks the expectation for Exp!

Now, s_constructible<T, U&&> is true if it's possible to direct-initialize a T from a U&& - if T(std::declval<U&&>()) is a valid expression.

is_convertible<U&&, T>, on the other hand, checks if it is possible to copy-initialize a T from a U&&. That is, if T copy() { return std::declval<U&&>(); } is valid.

The difference is that the latter does not work if the conversion is explicit:

+-----+--------------------------+------------------------+
|     | is_constructible<T, int> | is_convertible<int, T> |
+-----+--------------------------+------------------------+
| Imp |        true_type         |       true_type        |
| Exp |        true_type         |       false_type       |
+-----+--------------------------+------------------------+

In order to correctly propagate explicitness, we need to use both traits together - and we can create meta-traits out of them:

template <class T, class From>
using is_explicitly_constructible = std::integral_constant<bool,
    std::is_constructible<T, From>::value &&
    !std::is_convertible<From, T>::value>;

template <class T, class From>
using is_implicitly_constructible = std::integral_constant<bool,
    std::is_constructible<T, From>::value &&
    std::is_convertible<From, T>::value>;

These two traits are disjoint, so we can write two constructor templates that are definitely not both viable, where one constructor is explicit and the other is not:

template <class T>
class wrapper {
public:
    template <class U, REQUIRES(is_explicitly_constructible<T, U&&>::value)>
    explicit wrapper(U&& u) : val(std::forward<U>(u)) { }

    template <class U, REQUIRES(is_implicitly_constructible<T, U&&>::value)>
    wrapper(U&& u) : val(std::forward<U>(u)) { }
private:
    T val;
};

This gives us the desired behavior:

wrapper<Imp> wi = 0; // okay, calls non-explicit ctor
wrapper<Exp> we = 0; // error
wrapper<Exp> we2(0); // ok

This is what the implementation is doing here - except instead of the two meta-traits they have all of the conditions written out explicitly.

like image 106
Barry Avatar answered Oct 10 '22 00:10

Barry