More converting constructors for Optional?

246 views
Skip to first unread message

Andrzej Krzemieński

unread,
Jun 20, 2014, 6:22:42 AM6/20/14
to std-pr...@isocpp.org
Hi Everyone,
I am considering adding more converting constructors to std::experimental::optional. The following lines currently do not work, but they would with my addition:

optional<const int> oi = optional<int>(2); // const T != T
optional
<unique_ptr<Base>> op = optional<unique_ptr<Derived>>();

There are similar and even more issues with mixed assignment and comparison. However, there is one problem with this addition that I am not sure how to solve elegantly. I need your advice here.
What should be the expected result in the following 2 cases:

// case 1
struct Tool
{
 
Tool(optional<int>);
};

optional
<int> oi = 1;
optional
<Tool> ot = oi;
Error or call optional<T>(U const&) ?

// case 2
struct Tool
{
 
Tool(int); // ctor 1
 
Tool(optional<int>); // ctor 2
};

optional
<int> oi = 1;
optional
<Tool> ot = oi;
Error, or use ctor 1, or use ctor 2?

The most elegant solution I was able to find so far is this:

template <typename T>
class optional
{
  optional
(T const&);
  optional
(T &&);

 
template <typename U>
  optional
(optional<U> &&) requires !is_convertible<optional<U>, T>::value;

 
template <typename U>
  optional
(optional<U> const&) requires !is_convertible<optional<U>, T>::value;
};

This makes case 1 work intuitively and makes case 2 a compiler error.
Any opinions are welcome.

Regards,
&rzej
 

Roman Perepelitsa

unread,
Jun 20, 2014, 7:21:09 AM6/20/14
to std-pr...@isocpp.org
I like it. I believe the 'requires' clause on the second overload should read !is_convertible<const optional<U>&, T>::value.

What about the following example?
struct Tool
{
 explicit 
Tool(int);

};

optional<int> oi = 1;
optional<Tool> ot(oi);
Error or call optional<T>(optional<U>&&)?
Roman Perepelitsa.

Andrzej Krzemienski

unread,
Jun 20, 2014, 8:18:31 AM6/20/14
to std-pr...@isocpp.org
2014-06-20 13:20 GMT+02:00 Roman Perepelitsa <roman.pe...@gmail.com>:

What about the following example?
struct Tool
{
 explicit 
Tool(int);

};

optional<int> oi = 1;
optional<Tool> ot(oi);
Error or call optional<T>(optional<U>&&)?
I am not worried about this problem. It is orthogonal to the one I described above, and it can be solved with a "conditionally explicit constructor" as described by Daniel Krügler in: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3680.html


Regards,
&rzej

toma...@gmail.com

unread,
Jun 21, 2014, 4:08:52 AM6/21/14
to std-pr...@isocpp.org
At the first thought I support the compilation error, because I do not have any intuition on which of the constructor should be preferred, but maybe that is only matter of being familiar with the new optional design, so I will try to analyze the situation from objective view.

We have two constructors:
unboxing: optional<T> from optional<U>, that constructs T from U if optional is initialized
forwarding: optional<T> from V, that constructs T from U
The problem is that if T can be constructed both from U and optional<U> we have ambiguity, now we can select one of 3:
1) Compilation error and let the programmer decide
2) Preffer unbonxing constructor
3) Preffer forwarding constructor

1. Let analyze all options basing on the ability of the programmer to select specified constructor if other is prefferd (on non of them). Let assume that optional<U> ou
1) to use unboxing: in the current state of proposal we do not have any make_if function, so the only way is to use if.
  optional<T> ot = ou ? optional<T>{in_place, *ou} : optional<T>{};
2) to use forwarding:
  optional<T> ot{ in_place, ou }
This suggest that unboxing constructor should be preffered, because we provide an elegant way to force us of forwarding constructor. But this argument want apply if we provide a make_if function/constructor.

2. If we choose one of the preference path, then we may introduce a hard to find error cause by silent code behavior change if the constructor is added to overload set. Let assume we have following:
  class Test { Test(optional<int>); }
  optional<Test> ot = optional<int>{};
  //In this example the ot will be always initialized with optional<int> because the forwarding constructor is only match.

But if we add another constructor to the class Test and assume preference of unboxing:
  class Test { Test(double); Test(optional<int>); }
  optional<Test> ot = optional<int>{};
  //In this example the ot will not be initialized, because unboxing is preffered.

This silent code changes may cause very subtle and hard to find errors that will be introduced silently. This argument suggest that we should go with compilation error path.

3. We the current design the meaning of construction of optional<T> from optional<U> depends of set of constructors that is available for type T.
The problem is that this set is not clearly visible for the programmer at the point of doing such construction and may change from the point when code was written and introduce errors.
This suggest our jest another solutions -> optional will always do unboxing, despite the available constructors of T.
unboxing: optional<T> from optional<U>, requires that U is constructible from T
forwarding: optional<T> from V, requires that V is not optional and T is constructible from V
That will combine benefits from both point 1 and 2.

In case of your examples:
//case 1

optional<int> oi = 1;
optional<Tool> ot = oi; //error, Tool is not construtible from int
optional<Tool> ot{in_place, oi}; //ok
optional<Tool> ot = make_optional(oi); //ok, optional<optional<int>> is unboxed


//case 2

optional<int> oi = 1;
optional<Tool> ot = oi; //ok, selects Tool(int) because of unboxing
optional<Tool> ot{in_place, oi}; //ok, selets Tool(optional<int>)
optional<Tool> ot = make_optional(oi); //ok, optional<optional<int>> is unboxed


toma...@gmail.com

unread,
Jun 23, 2014, 11:31:45 AM6/23/14
to std-pr...@isocpp.org
I want to also mention one thing, if the optional will have following set of constructors:
template <typename T>
class optional
{

  //from-T
  optional
(T const&);
  optional
(T &&);

  //unboxing
 
template <typename U>
  optional
(optional<U> &&) requires is_construtible<T, U>::value;

 
template <typename U>
  optional
(optional<U> const&) requires is_construtible<T, U>::value;

  //forwarding
 
template <typename U>
  optional(U&&) requires
is_construtible<T, U&&>::value && !is_optional<U>::value;
};

The from-T constructor seems to be an extesion to forwarding constructor, but they existence may cause the always unboxing rule to not be actually aplied (they wont be applied with optional<optional<T>>). For example if we have:
  optional<double> const os{};
  optional<optional<double>> od = os; //The from-T constructor will apply and od will be initialized with uninitialized optional as value.

But if we have very similar code:
  optional<int> const os{};
  optional<optional<double>> od = os; //The unboxing constructor will apply and od will not be initialized.

If the os is intialized the behaviour of both constructors will be the same - od will be intialized with intialized optional as value.

To fix the problem I propose to remove the from-T constructor and leave only unboxing and forwarding constructor, because the forwarding constructor can replace from-T constructors. Also default generated move and copy constructor for optional, behaves exactly like unboxing ones, so there are no problem with them.

Andrzej Krzemieński

unread,
Jul 7, 2014, 10:24:01 AM7/7/14
to std-pr...@isocpp.org

One problem with having such "from any convertible U" constructor has been discussed in the past. See this post and the subsequent discussion:

https://groups.google.com/a/isocpp.org/d/msg/std-proposals/uK1HfW4YTEU/u_wZWh-2KzMJ

In short, having this constructor can introduce a number of overload resolution ambiguities:

void fun(optional<T> const& v);
void fun(T const& v);

U u
;
fun
(u);

U is convertible to T, so both candidates are viable.

 

toma...@gmail.com

unread,
Jul 7, 2014, 11:33:09 AM7/7/14
to std-pr...@isocpp.org
W dniu poniedziałek, 7 lipca 2014 16:24:01 UTC+2 użytkownik Andrzej Krzemieński napisał:

One problem with having such "from any convertible U" constructor has been discussed in the past. See this post and the subsequent discussion:

https://groups.google.com/a/isocpp.org/d/msg/std-proposals/uK1HfW4YTEU/u_wZWh-2KzMJ

In short, having this constructor can introduce a number of overload resolution ambiguities:

void fun(optional<T> const& v);
void fun(T const& v);

U u
;
fun
(u);

U is convertible to T, so both candidates are viable.

This ambiguity problem may be fixed by making the the function that takes a T an template:
namespace detail
{
  void fun(T const&)
}

template<typename U>

  requires is_construtible<T, U&&>::value && !is_optional<U>::value
inline void fun(U&& u)
{
  return f(std::forward<U>(u));
}

void fun(optional<T> const& t);

To avoid difference in observerable behaviour between code that define only optional<T> overload and the one that has T overload implemented as a template, the template overload should be has the same restrictions as forwarding optional constructor (to preserve preference of unboxing rule).

toma...@gmail.com

unread,
Jul 7, 2014, 11:51:03 AM7/7/14
to std-pr...@isocpp.org
Actually I think I would recommend following as "pattern" aproach to such overloaded functions.
namespace impl
{
  void fun(T const&);
  void fun(nullopt_t);
}

//is_optional<nullopt_t>::value should be true

template<typename U>
  requires is_construtible<T, U&&>::value && !is_optional<U>::value
inline void fun(U&& u)
{
  return impl::fun(std::forward<U>(u));

}

void fun(optional<T> const& t)
{
   return t ? impl::fun(*t) : impl::fun(nullopt);
}
This quarantines that both overloads has the same behaviour if t is present.

toma...@gmail.com

unread,
Jul 8, 2014, 12:22:36 PM7/8/14
to std-pr...@isocpp.org
I think that library can provide a helper class named tentatively named handle_value_or_optional that will accepts value of type convertible with the and two functors: one accepting the actual value and with no argument for handling no value. In addition it should provide is_value_or_optional_compatible_with functor.  That will make implementation of such functors easier from user presepective:
namespace impl
{
  void fun(T const&);
  void fun(nullopt_t);
}


template<typename U>
  requires is_value_or_optional_compatible_with<U&&, T const&>
inline void fun(U&& u)
{
  return
handle_value_or_optional<T const&>(
           std::forward<U>(u),
           [](T const& t) { return impl::fun(t); },
           [] { return impl::fun(nullopt); }
}

The example implementation of handle_value_or_optional:
template<typename T, typename U, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional_impl(U&& u, FV&& fv, FN&&)
  requires !is_optional_v<decay_t<U>> && is_convertible_v<U&&, T>
{ return std::forward<FV>(fv)(std::forward<U>(u)); }

/* Avoid constucting optional<T> from optional<U> and use prefect forwarding */
template<typename T, typename U, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional_impl(optional<U>&& u, FV&& fv, FN&& fn)
  requires is_optional_v<decay_t<U>> && is_convertible_v<U&&, T>
{
  return u ? std::forward<FV>(fv)(std::move(*u)) : std::forward<FN>(fn)();
}

template<typename T, typename U, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional_impl(optional<U> const& u, FV&& fv, FN&& fn)
  requires is_optional_v<decay_t<U>> && is_convertible_v<U const&, T>
{
  return u ? std::forward<FV>(fv)(*u) : std::forward<FN>(fn)();
}

template<typename T, typename U, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional_impl(optional<U>& u, FV&& fv, FN&& fn)
  requires is_optional_v<decay_t<U>> && is_convertible_v<U&, T>
{
  return u ? std::forward<FV>(fv)(*u) : std::forward<FN>(fn)();
}

/* Avoid if for fun(nullopt) case */
template<typename T, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional_impl(nullopt_t, FV&&, FN&& fn)
{
  return std::forward<FN>(fn)();
}

template<typename T, typename U, typename FV, typename FN>
inline decltype(auto) handle_value_or_optional(U&& u, FV&& fv, FN&& fn)
  requires is_value_or_optional_compatible_with_v<U&&, T>
{
  return handle_value_or_optional_impl<T>(std::forward<U>(u), std::forward<FV>(fv), std::forward<FN>(fn));
}

Michał Dominiak

unread,
Jul 8, 2014, 12:30:57 PM7/8/14
to std-pr...@isocpp.org

Since you have already mostly reinvented them (and so did future's .then), can we get convenient syntax for monadic bind already?

--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

toma...@gmail.com

unread,
Jul 8, 2014, 12:41:41 PM7/8/14
to std-pr...@isocpp.org
W dniu wtorek, 8 lipca 2014 18:30:57 UTC+2 użytkownik Michał Dominiak napisał:

Since you have already mostly reinvented them (and so did future's .then), can we get convenient syntax for monadic bind already?

Could you please elaborate a bit how this solution can be mapped to monadic bind? And to .then? I am not familiar with this concept. Most preferably if you have links to same papers, articles or presentation on this topic.

Also jsut after posting I found that T argument is not necessary for handle_value_or_optional, because I placed the checking on function that uses it. So it can look like:

namespace impl
{
  void fun(T const&);
  void fun(nullopt_t);
}


template<typename U>
  requires is_value_or_optional_compatible_with<U&&, T const&>
inline void fun(U&& u)
{
  return handle_value_or_optional(

           std::forward<U>(u),
           [](T const& t) { return impl::fun(t); },
           [] { return impl::fun(nullopt); });
}

Bjorn Reese

unread,
Jul 8, 2014, 1:30:58 PM7/8/14
to std-pr...@isocpp.org
On 07/08/2014 06:30 PM, Michał Dominiak wrote:
> Since you have already mostly reinvented them (and so did future's
> .then), can we get convenient syntax for monadic bind already?

N4015

Michał Dominiak

unread,
Jul 8, 2014, 1:46:05 PM7/8/14
to std-pr...@isocpp.org
You should look for the Haskell tutorial on monads.

My point was, if `optional` gets something similar to what you are talking about, it basically mostly becomes a monad. Similarly, when `future` gets .then(), it becomes a monad, since .then() is pretty much a monadic bind on it.

As for N4015, yes, that's another monad, which only makes it more obvious we need something akin to Haskell's >>= (heck, we could even get exactly >>= for this); otherwise, we will keep inventing new and new names for different realizations of the same construct, and the code will get harder and harder to read, while with special syntax for monadic bind, the code is pretty much obvious, even if you don't know the particular monad that is used by it.

Reply all
Reply to author
Forward
0 new messages