5

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?

Curious
  • 20,870
  • 8
  • 61
  • 146
  • 1
    "*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)*" That isn't true. A template argument cannot deduce an `initializer_list` when passed a braced-init-list directly. At least, not without explicitly doing something like `initializer_list`. – Nicol Bolas Dec 25 '17 at 06:03
  • @NicolBolas Right so in that case (when the user passes an initializer list), the type falls back to the default type (because the function template cannot deduce the `initializer_list`), and then the expression in curly braces is matched against the constructors of the underlying type to see if there is a match. – Curious Dec 25 '17 at 06:05
  • ... really? Do you happen to know where the specification specifies this behavior? It seems kind of counter-intuitive. – Nicol Bolas Dec 25 '17 at 06:07
  • @NicolBolas I was surprised when I stumbled upon this as well https://wandbox.org/permlink/eCO7PBetfXOn7qwG never bothered looking up where this behavior was documented in the standard. I did think it was a pretty neat trick though. If you happen to find out where this is specified in the standard then I would be interested in knowing as well – Curious Dec 25 '17 at 06:10
  • @NicolBolas I guess it's `[temp.decls]`p2? *For purposes of name lookup and instantiation, default arguments and noexcept-specifiers of function templates and default arguments and noexcept-specifiers of member functions of class templates are considered definitions* – Curious Dec 25 '17 at 06:32
  • 1
    I'd say it's more [temp.deduct.call]/1: "*Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context*" Where "non-deduced context" is [temp.deduct.type], and paragraph 5.4 says, "*A template parameter used in the parameter type of a function parameter that has a default argument that is being used in the call for which argument deduction is being done.*" – Nicol Bolas Dec 25 '17 at 06:36

2 Answers2

5

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
T.C.
  • 133,968
  • 17
  • 288
  • 421
1

I got an answer to this as soon as I posted the question (it's included in the last part of my question as an edit)

The library wants to give users a convenient way to be able to detect whether an optional can be constructed via that constructor in SFINAE situations without causing hard errors later. If that constraint was not included then libraries might think that the optional can be constructed from any other type but later find out that it results in a hard error.

For example the following example illustrates a similar problem (https://wandbox.org/permlink/XGgWgJcNJ99BBycu)

#include <iostream>
#include <string>
#include <vector>
#include <tuple>

using std::cout;
using std::endl;

class Something {
public:
    Something() = default;
    template <typename U>
    Something(U&& u) : vec(std::forward<U>(u)) {}
private:
    std::vector<int> vec;
};

template <typename U, typename = std::void_t<>>
class SomethingConstructibleFrom 
    : public std::false_type {};
template <typename U>
class SomethingConstructibleFrom<
        U, 
        std::void_t<decltype(Something{std::declval<U>()})>> 
    : public std::true_type {};

int main() {
    if constexpr (SomethingConstructibleFrom<std::string>::value) {
        // this must be constructible because the above returned true
        // but SURPRISE!
        auto something = Something{std::string{"qwerty"}};
        std::ignore = something; 
    }
}
Curious
  • 20,870
  • 8
  • 61
  • 146