1

In SQL, COALESCE(val_1, val_2, ... val_n) is a variadic function which returns its first non-null argument, or null otherwise.

Now, in C++, we have pointers which can be null, but also std::optional's since C++17, and there's std::variant which could be null or hold the monostate type.

Is there some standard library function in C++ which accepts multiple nullable (or emptyable) objects, and returns the first non-null? If not, has one been proposed, e.g. in the context of monadic programming perhaps?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • It's non-standard, but gcc and clang both offer a `?:` operator, where `a ?: b` has the value `a`, unless it is null/0/false, in which case it has the value `b`. Effectively equivalent to `a ? a : b`, except `a` is evaluated only once. – Drew Dormann Aug 12 '22 at 21:02

1 Answers1

3

Not aware of any, but you can easily write one:

template<typename T>
auto coalesce(T&& t) { return *t; }

template<typename T1, typename... Ts>
auto coalesce(T1&& t1, Ts&&... ts)
{
    if (t1)
    {
        return *t1;
    }
    return coalesce(std::forward<Ts>(ts)...);
}

You can also make it an operator (e.g. in a namespace, so you don't pollute global operators) or make it a custom operator.


The above solves the problem when you pass nullable types. Upon request, here's a version that works for the case when you'd like to mix nullable and non-nullables - for non-nullables, it'll return the first non-nullable, of course. This uses C++20:

template<typename T1, typename... Ts>
auto coalesce(T1&& t1, Ts&&... ts)
{
    if constexpr (requires { *t1; })
    {
        if constexpr (sizeof...(ts))
        {
            return coalesce(std::forward<Ts>(ts)...);
        }
        return *t1;
    }
    return std::forward<T1>(t1);
}

Note that even this version has the issue that you can only call it with one underlying type (as in, std::common_type<T1, Ts...> must exist). If you'd like to be able to process multiple (potentially) unrelated underlying types, you can resort to continuation passing:

template<typename Cont, typename T1, typename... Ts>
auto coalesce(const cont& c, T1&& t1, Ts&&... ts)
{
    if constexpr (requires { *t1; })
    {
        if constexpr (sizeof...(ts))
        {
            return coalesce(cont, std::forward<Ts>(ts)...);
        }
        return cont(*t1);
    }
    return cont(std::forward<T1>(t1));
}
lorro
  • 10,687
  • 23
  • 36
  • This doesn't distinguish nullness/emptiness from falseness... a very common bug in weakly-typed languages :-P – einpoklum Aug 12 '22 at 21:07
  • @einpoklum In C++ and for the types OP asked, you first check for nullness, then use dereference op, and then check for value (i.e., falseness). So no problem here. Of course, you might assert that e.g. `operator*` is available on `t`/`t1`, but I didn't see a reason to divert with that. – lorro Aug 12 '22 at 21:08
  • Your code doesn't do that, it just checks for falseness. – einpoklum Aug 12 '22 at 21:10
  • @einpoklum So. I expect it to be called only on nullable types. As types are known to the caller I only see the reason to do so for those types. If you'd like to include support for non-nullable types, you might simply start the second function with a check for `t1::operator*()`'s existence (`is_detected`) and return `t1` forwarded if `operator*()` is not available. Does that answer your question? – lorro Aug 12 '22 at 21:13
  • How does this figure out the return type of the second `coalesce`? Naively, I would think that the return type would depend at runtime on which `ts`, if any, is the first non-null value, but of course the actual type of `auto` has to be known at compile time. And in fact when I try this [on godbolt](https://godbolt.org/z/Mzd1q4Y1q) I get exactly the failure to compile I'd expect. – Nathan Pierson Aug 12 '22 at 21:28
  • 1
    @NathanPierson Here the types must either be the same or have a common type (as in `std::common_type<>`, i.e., don't expect it to work with `int*` and `float*`). Hence I didn't understand einpoklum's request first: you likely call it with only nullables of the same type. However, via cps you can have very different types. Added an example on that as well. – lorro Aug 12 '22 at 21:37