Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constraint on std::optional's forwarding reference constructor

std::optional has 8 constructors as of this date, listed below (also here http://en.cppreference.com/w/cpp/utility/optional/optional)

/* (1) */ constexpr optional() noexcept;
/* (1) */ constexpr optional( std::nullopt_t ) noexcept;

/* (2) */ constexpr optional( const optional& other );

/* (3) */ constexpr optional( optional&& other ) noexcept(/* see below */);

template < class U >
/* (4) */ /* EXPLICIT */ optional( const optional<U>& other );

template < class U >
/* (5) */ /* EXPLICIT */ optional( optional<U>&& other );

template< class... Args > 
/* (6) */ constexpr explicit optional( std::in_place_t, Args&&... args );

template< class U, class... Args >
/* (7) */ constexpr explicit optional( std::in_place_t,
                                       std::initializer_list<U> ilist, 
                                       Args&&... args );

template < class U = value_type >
/* (8) */ /* EXPLICIT */ constexpr optional( U&& value );

I like the last constructor. It helps std::optional to be constructed from cv-ref qualified references to type Type. Which is super convenient.

Other than that, the last constructor also helps because it is a convenient way to use list initialization to initialize the std::optional instance, without having to use std::in_place. This happens because when a curly brace enclosed argument list is passed to the constructor, the default type is used because the function template cannot deduce a type from the {} (at least that is my understanding of the situation and is a neat trick I picked up only recently) (also note that this can only be used to call non explicit constructors of the underlying type, as per the rules here http://en.cppreference.com/w/cpp/language/list_initialization)

auto optional = std::optional<std::vector<int>>{{1, 2, 3, 4}};

There are two constraints on the last constructor that I can understand

  • std::decay_t<U> is neither std::in_place_t nor std::optional<T>
  • This constructor is explicit if and only if std::is_convertible_v<U&&, T> is false

The first is easy to understand, it helps prevent against ambiguities with constructors (2), (3), (4), (5), (6) and (7). If the type is std::in_place it can conflict with (6) and (7). If the type is an instantiation of std::optional then it can conflict with (2), (3), (4) and (5).

The second just "forwards" the explicitness of the constructor of the underlying type to the optional type

But the third restriction is curious

  • This constructor does not participate in overload resolution unless std::is_constructible_v<T, U&&> is true

Why is this needed? (8) can never conflict with the empty constructor because it needs at least one argument. That leaves only one left reason - it might conflict with std::nullopt_t when passed std::nullopt, but that will not happen because the nullopt version is always a better match no matter what cv-ref qualified version of std::nullopt_t is passed (as demonstrated below)

void func(int) {
    cout << __PRETTY_FUNCTION__ << endl;
}

template <typename U>
void func(U&&) {
    cout << __PRETTY_FUNCTION__ << endl;
}

int main() {
    auto i = int{1};
    func(1);
    func(i);
    func(std::move(i));
    func(std::as_const(i));
    func(std::move(std::as_const(i)));
}

What is the reason behind the last restriction?

Why not just let the constructor error out as usual? Is this needed to help detect if the type is constructible via an argument passed via SFINAE without causing a hard error later?

like image 930
Curious Avatar asked Dec 25 '17 05:12

Curious


1 Answers

Lying traits are ungood.

Lying traits for a fundamental vocabulary type are plusungood.

Lying traits for a fundamental vocabulary type that can also easily interfere with overload resolution are doubleplusungood.

void f(std::optional<int>);
void f(std::optional<const char*>);
f({""}); // ambiguous without the constraint
like image 140
T.C. Avatar answered Sep 23 '22 01:09

T.C.