Alright, I messed up asking this question the first time.
Is there a way, idiomatically, to provide a constructor which takes one or more std::optional<T> and returns a std::optional<U>? For instance, ideally I would love some kind of syntax like
#include <optional>
struct Rational {
explicit Rational(int i) : num{i}, denom{1} {}
Rational(int i, int j) : num{i}, denom{j} {}
// magic here?...
int num, denom;
};
int main()
{
Rational i1 = Rational(1); // the number 1
Rational h2 = Rational(1,2); // the number 1/2
std::optional<int> opt_i{}, opt_j{};
std::optional<Rational> e3 = std::optional<Rational>(opt_i);
// empty, converting copy constructor
/* std::optional<Rational> e4 = std::optional<Rational>(opt_i, 2); */
/* std::optional<Rational> e5 = std::optional<Rational>(2, opt_i); */
/* std::optional<Rational> e6 = std::optional<Rational>(opt_i, opt_j); */
// error, no constructor
// ideally would construct empty std::optional<Rational>
opt_i = 3;
std::optional<Rational> i7 = std::optional<Rational>(opt_i);
// contains Rational(3), the number 3
/* std::optional<Rational> r8 = std::optional<Rational>(opt_i, 2); */
/* std::optional<Rational> r9 = std::optional<Rational>(2, opt_i); */
// error, no constructor
// ideally would construct std::optional<Rational> containing 3/2 and 2/3, respectively
opt_j = 4;
/* std::optional<Rational> r10 = std::optional<Rational>(opt_i, opt_j); */
// error, no constructor
// ideally would construct std::optional<Rational> containing 3/4
}
Live demo
Thanks to 463035818_is_not_an_ai's answer on the first question, we know that for simple converting/copy constructors std::optional provides an overload for its constructor. For general constructors, I believe this is not currently possible. What are my best options?
With C++17, mostly yes. But there are a few caveats. Here's the secret sauce.
template <typename C>
struct OptionalConstructor {
template <typename... Ts>
std::optional<C> operator ()(std::optional<Ts>... args) const {
if ((... && args)) {
return C((*args)...);
} else {
return std::nullopt;
}
}
};
We could have written this as a single function taking template <typename C, typename... Ts>, but then you'd always have to specify your template arguments if you wanted to specify C, which isn't really what you want. So by splitting it into a constructor (whose type takes C) and a call operator (which will infer Ts...), we can omit the Ts... part in most cases.
So what does this do? C++17 introduced a neat little feature called fold expressions, which is what gives us that nice short if statement.
if ((... && args)) {
...
}
This says "if all of the arguments are true in Boolean context". In that case, we forward all of our arguments, dereferenced (which we now know is safe), to the constructor for C. If not, we return nullopt, the empty optional.
Example usage:
OptionalConstructor<Rational> opt_rational;
std::optional<int> opt_i{}, opt_j{};
opt_rational(opt_i); // Empty optional
opt_rational(opt_i, opt_j); // Empty optional
opt_i = 3;
opt_rational(opt_i); // Fraction 3/1
opt_rational(opt_i, opt_j); // Empty optional
opt_j = 2;
opt_rational(opt_i, opt_j); // Fraction 3/2
Now the caveat is that this only works if you're passing all std::optional values. Some of your examples involve mixed types, which won't work.
// Does NOT correctly infer template arguments:
opt_rational(opt_i, 2);
C++ won't try to do the implicit conversion from int to std::optional<int> for the second argument since it won't infer the template argument in that case. We can provide explicit template arguments, but it's not pretty.
opt_rational.operator()<int, int>(opt_i, 2);
Now C++ knows that the second argument must be std::optional<int> for certain, so it's happy to do the implicit conversion. We can clean that up a little bit if we define a create member function that delegates to operator()
// Inside struct OptionalConstructor
template <typename... Ts>
std::optional<C> create(std::optional<Ts>... args) const {
return (*this)(args...);
}
Then we can write
opt_rational.create<int, int>(opt_i, 2);
which is, at least, a little better.
By the way, while this won't help you in C++ specifically, there is a word for what you're trying to do. You're taking an applicative functor (which std::optional is an excellent example of) and a function that doesn't use the applicative functor, and you're lifting the argument and result types into the applicative functor. These are called applicative brackets, or sometimes idiom brackets.
You can use a trait class to naturally extract the value you need depending on the type.
//Not needed in C++20
template <typename T>
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
template <typename T>
struct OptionalTrait{
using value_type = T;
static bool has_value(const T& v){
return true;
}
template <typename U>
static U&& value(U&& v){
return std::forward<U>(v);
}
};
template <typename T>
struct OptionalTrait<std::optional<T>>{
using value_type = typename OptionalTrait<T>::value_type;
static bool has_value(const std::optional<T>& v){
return v.has_value() && OptionalTrait<T>::has_value(v.value());
}
template <typename U>
static decltype(auto) value(U&& v){
return OptionalTrait<T>::value(std::forward<U>(v).value());
}
};
The traits are designed in the most general manner, taking into considerations of perfect forwarding, and also works for nested std::optionals (e.g. std::optional<std::optional<int>>).
Then, we can use the trait to write a factory function that does the correct thing.
template <typename T, typename... Ts>
std::optional<T> makeFromOptional(Ts&&... args){
if((OptionalTrait<remove_cvref_t<Ts>>::has_value(args) && ...)){
return T{OptionalTrait<remove_cvref_t<Ts>>::value(std::forward<Ts>(args))...};
}
else{
return std::nullopt;
}
}
This makes all the cases in the question work as expected.
auto e4 = makeFromOptional<Rational>(opt_i, 2);
assert(!e4);
auto e5 = makeFromOptional<Rational>(2, opt_i);
assert(!e5);
auto e6 = makeFromOptional<Rational>(opt_i, opt_j);
assert(!e6);
opt_i = 3;
std::optional<Rational> i7 = std::optional<Rational>(opt_i);
assert(i7->num==3 && i7->denom==1);
std::optional<Rational> r8 = makeFromOptional<Rational>(opt_i, 2);
assert(r8->num==3 && r8->denom==2);
std::optional<Rational> r9 = makeFromOptional<Rational>(2, opt_i);
assert(r9->num==2 && r9->denom==3);
opt_j = 4;
auto r10 = makeFromOptional<Rational>(opt_i, opt_j);
assert(r10->num==3 && r10->denom==4);
Additionally, this also work,
std::optional<std::optional<int>> opt_k{5};
auto e7 = makeFromOptional<Rational>(opt_k, 2);
assert(e7->num==5 && e7->denom==2);
Demo: https://godbolt.org/z/6ro3bzzcK
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With