Search code examples
c++c++17stdoptional

Pass-through constructor for std::optional arguments


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?


Solution

  • 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.