4

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?

3
  • isnt this coverd by this answer already stackoverflow.com/a/77864843/4117728 ? Commented Jan 23, 2024 at 18:14
  • @463035818_is_not_an_ai yes, I asked them to post the answer here so I can accept it. Your answer was most appropriate for the question I asked incorrectly, but theirs is the answer to the question I meant to ask. Commented Jan 23, 2024 at 18:16
  • Is static factory function an option on the table? Commented Jan 23, 2024 at 18:33

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

2 Comments

This seems like a way (using the general unholiness which are immediately-invoked lambdas) to mix std::optional and non-std::optional arguments. Thoughts?
Though I probably need to throw in some universal references and std::forwards...
2

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

1 Comment

This might give you some ideas... godbolt.org/z/hxh83P3oo
1

Just to put what works with the current design of std::optional:

#include<optional>

struct A {};

struct B {
    B(A const&) {}
};

int main() {
    std::optional<A> a;
    std::optional<B> b{a?std::optional<B>{*a}:std::nullopt};
}

https://compiler-explorer.com/z/s73MjMo3j

Optional could have been designed to support this, but it is a can of worms.

A general solution would be to a protocol for functional propagation which is in C++23

    std::optional<B> b = a.transform([](auto const& a) {return B{a};});

https://compiler-explorer.com/z/EhPaa145c

Or, more generally, a covariant propagation that I have for std::variant (not std::optional at the moment, sorry).

Here is the example with monostate, which works like the empty state in the variant.

(with https://gitlab.com/correaa/boost-covariant , not an official Boost library)

#include<boost-covariant/covariant.hpp>  // from https://gitlab.com/correaa/boost-multi

std::variant<std::monostate, A> a;
std::variant<std::monostate, B> b =
  boost::covariant{
    [](A const& a) {return B{a};},
    [](auto&&) {}  // returns void
  }(a);

The type can be deduced:

auto b =
  boost::covariant{
    [](A const& a) {return B{a};},
    [](auto&&) {}  // returns void, maps to monostate
  }(a);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.