7
\$\begingroup\$

I want to coalesce (lvalue references to) optionals of the same type, in C++17:

#include <optional>

template <typename... Ts>
constexpr auto coalesce(std::optional<Ts>&... optionals );

template <typename T>
constexpr std::optional<T>& coalesce(std::optional<T>& x, std::optional<T>& y)
{
    return x.has_value() ? x : y;
}

template <typename T>
constexpr std::optional<T> coalesce(std::optional<T>& x)
{
    return x;
}

template <typename T, typename... Ts>
constexpr std::optional<T> coalesce(std::optional<T>& x, std::optional<Ts>&... xs )
{
    return coalesce(x, coalesce(std::forward<std::optional<T>&>(xs)...));
}

This is sub-optimal, as it uses recursive templated function calls, rather than ellipsis-based expansion in a single body. What would you suggest?

Notes:

  • C++17 and no fancy libraries please.
  • This can be seen as a specific case of the "return first parameter in a pack satisfying a predicate", with the predicate being [](auto x) { return x.has_value(); }
  • If possible, let's not copy any T's.
\$\endgroup\$
0

3 Answers 3

9
\$\begingroup\$

So ideally you want to write something like:

template <typename... Ts>
constepxr auto coalesce(std::optional<Ts>&... xs)
{
   return (xs || ...);
}

But the issue here is of course that it will convert the optionals to bool before applying the boolean or-operator. You could however cast it to a type that does provide the right semantics for the boolean or-operator to do the thing you want:

template <typename T>
struct coalescable {
    T &x;
    coalescable(T &x_): x(x_) {}
    constexpr const coalescable &operator||(const coalescable &other) {
        return x.has_value() ? *this : other;
    }
};

And then use that in the fold expression:

template <typename... Ts>
constepxr auto coalesce(std::optional<Ts>&... xs)
{
   return (coalescable(xs) || ...).x;
}
\$\endgroup\$
7
  • 1
    \$\begingroup\$ It's too bad we can't define templated classes within functions. \$\endgroup\$
    – einpoklum
    Commented Dec 21, 2019 at 21:16
  • \$\begingroup\$ @einpoklum-reinstateMonica There is no problem an additional level of indirection cannot solve. \$\endgroup\$ Commented Dec 21, 2019 at 22:16
  • \$\begingroup\$ @einpoklum-reinstateMonica we can't? \$\endgroup\$ Commented Dec 22, 2019 at 6:08
  • \$\begingroup\$ @NooneAtAll: Lambdas excluded from that statement obviously, but - no, we can't. \$\endgroup\$
    – einpoklum
    Commented Dec 22, 2019 at 12:01
  • 1
    \$\begingroup\$ Nice solution! Just a small nitpick: The original code returned std::optional<T>& (note the reference!), but your final example returns the optionals by value (since auto doesn't deduce references). This can easily be fixed by changing the return type of coalesce to auto& (preferred in this case) or decltype(auto). \$\endgroup\$
    – hoffmale
    Commented Dec 22, 2019 at 15:52
9
\$\begingroup\$

Folding? Bah. New-fangled nonsense. Real programmers use for loops for for looping.

#include <optional>
#include <functional>

template<class T, class... Ts>
std::optional<T> coalesce(std::optional<Ts>&... xs)
{
    for (auto const& x : { std::reference_wrapper<std::optional<T>>(xs)... })
        if (x.get().has_value())
            return x;

    return {};
}

(Yeah, G. Sliepen's is better, and I still don't understand that other one).

\$\endgroup\$
4
  • \$\begingroup\$ Upvote for making me laugh with "real programmers". There is just one fly in the ointment: You require a manually specified return-type. \$\endgroup\$ Commented Dec 21, 2019 at 20:50
  • \$\begingroup\$ You can use the trick of @Deduplicator and write using T = std::common_type_t<Ts...>, so you can drop class T from the template arguments, and then use auto return type. \$\endgroup\$
    – G. Sliepen
    Commented Dec 21, 2019 at 21:08
  • \$\begingroup\$ So, in what ways (other than elegance) is @G.Sliepen's answer better? IIANM yours would work with C++14 and and std::experimental::optional, wouldn't it? \$\endgroup\$
    – einpoklum
    Commented Dec 21, 2019 at 21:11
  • 1
    \$\begingroup\$ I haven't seen a real programmers statement in decades! Thank you. \$\endgroup\$
    – pacmaninbw
    Commented Dec 22, 2019 at 11:24
5
\$\begingroup\$

Well, if you don't want recursion, you have to fold instead.

template <class... Ts>
constexpr auto coalesce(std::optional<Ts>&... xs) {
    std::optional<std::common_type_t<Ts...>> r;
    ((xs && (r = xs, true)) || ...);
    return r;
}

If you want to avoid even the single copy to the result, you need to change the result-type by using an optional std::reference_wrapper or going with a potentially null pointer.

template <class... Ts>
constexpr auto coalesce(std::optional<Ts>&... xs) noexcept {
    std::common_type_t<Ts...>* r = nullptr;
    ((xs && (r = &*xs, true)) || ...);
    if (r)
        return std::optional(std::ref(*r));
    return {};
}
\$\endgroup\$
5
  • \$\begingroup\$ Aren't you copying T's here? I didn't forbid it but it seems like it's better to avoid that. \$\endgroup\$
    – einpoklum
    Commented Dec 21, 2019 at 21:13
  • \$\begingroup\$ I was a bit confused about this at first too. But r is only assigned to once, and return value optimization avoids it from being a temporary copy. Of course it still returns a copy of one of the inputs, but so did your original code. I think all answers here could be modified to return a reference to one of the inputs, if that is what you want. \$\endgroup\$
    – G. Sliepen
    Commented Dec 21, 2019 at 21:28
  • \$\begingroup\$ @G.Sliepen: That's actually a bug in my code! I should have consistently returned references. Also, regardless - shouldn't r here be made a reference wrapper? \$\endgroup\$
    – einpoklum
    Commented Dec 21, 2019 at 21:35
  • 1
    \$\begingroup\$ Only if coalesce() should return a reference does r have to be a reference wrapper. \$\endgroup\$
    – G. Sliepen
    Commented Dec 21, 2019 at 21:41
  • \$\begingroup\$ @einpoklum-reinstateMonica If you really want to avoid even the copy to return-value, that needs std::reference_wrapper or returning a pointer. Your choice, added such an alternative. \$\endgroup\$ Commented Dec 21, 2019 at 22:28

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.