Just dropped in to see what condition my condition was in

There is quite a lot of C++ that is complicated, difficult to understand, easy to misuse, or all of the above. Of course people love to write about the complexities of something like initialization (guilty). Lately it’s apparently become in vogue to over-the-top fear-monger about reference semantic types (something I intend to write about in the future). But there’s one part of the language that is quite complicated yet just doesn’t quite get the love it deserves: the conditional operator, ?:(it’s so unloved that it’s quite common to misname it as the ternary operator).

It takes over a full page of the standard to explain what it does:

[expr.cond] from the latest working draft, N4741

The complexity comes from all sorts of things you can do with it. You can compare expressions of different value categories, of different types. One or both can be void! You can have bitfields? You can, of course, throw. We have bullets, we have sub-bullets, we have special cases for things like pointers to members and nullptr_t. There’s something for everyone in this section.

Now despite the wordiness here, cond ? a : b basically does what you’d expect and want it to do. Let’s say we have some Regular type T (I’m going to ignore cross-type conversions and void for the purposes of this post), there are six kinds of expressions as the second or third operand — the Cartesian product of the value categories (3: lvalue, xvalue, prvalue) and the cv-qualifiers (2: non-const and const, I’m also ignoring volatile for the purposes of this post). That makes 15 different potential pairings for a conditional expression to consider what the resulting type and value category should be. You don’t have to memorize what all 15 result in though — there are really only three important ones:

  • If both are glvalues and have the same type and value category (e.g. both are T& or both are T const&&), the result is that same type and value category.
  • If both are glvalues, but one’s type is more const than the other, the more const one is the type (e.g. if a is T& and b is T const&, then true ? a : b is T const&).
  • Otherwise, it’s a prvalue (just T).

A different way of looking at this — if it’s safe to get a reference, you get the most useful reference back. Otherwise, you get a value. A pretty handy feature!

Note that this isn’t 100% full-proof, you can definitely get a dangling reference out of it:

struct S { int i; };
decltype(auto) foo(int&& j) {
return true ? S{4}.i : std::move(j);

Because S{4}.i is an xvalue, we have two glvalues with the same type and value category (both are xvalues of type int), the result is also an xvalue of type int (in other words, x is an int&&). But S gets destroyed at the end of the expression, so we get a dangling reference (no lifetime extension in this case).

Now the conditional operator’s specification distinguishes between all the value categories, and can give you back any value category depending on its “arguments” — whether lvalue, xvalue, or prvalue. What I find particularly interesting is that this kind of careful distinguishing is impossible to do yourself.

That is, you cannot implement this function to have the same behavior as the equivalent use of the conditional operator:

template <typename A1, typename A2>
???? conditional_operator(bool, A1&&, A2&&);

Yes, you can’t implement it due to the evaluation guarantees that the conditional operator provides (exactly one expression is evaluated, which you cannot accomplish with a function). But you also can’t implement it due to the fact that you cannot, in a function template, distinguish between xvalues and prvalues (I’m discounting situations like having a macro that wraps your function call that explicitly passes in the value categories as non-deduced template parameters — because obviously). You can only distinguish between lvalues and rvalues. Whereas the conditional operator handled the full Cartesian product of 6 possible value category-types, in a function template we can only handle 4.

This leaves us with one of three options for deciding what the return type for this function should be (as before, let’s assume for simplicity that uncvref_t<A1> and uncvref_t<A2> are both T):

  1. We could always return a prvalue
  2. We could return an lvalue if both are lvalues, otherwise return a prvalue
  3. We could return an lvalue if both are lvalues, otherwise return an xvalue

The first and second options are always safe, but more expensive than necessary in some cases. The third never does a copy or move, at the cost of higher chance of ending up with a dangling reference. Whether or not that’s the right tradeoff depends on what kind of types you’re dealing with and how much the extra copy/move might or might not matter.

Which brings me, of course, to std::optional(which I believe I am contractually obligated to mention in every post?).

std::optional has a member function value_or, which either returns the underlying value of the optional (if it has one) or the provided argument (if it doesn’t). This is basically a conditional operator, but it’s a function, so it has to make the choice I listed above — and the C++ standard library chose option 1: it always returns a prvalue. This is the always-safe option. But if you’re worried about the potential cost, in cases where you are holding larger or more expensive-to-copy types, never fear. There’s a solution for you there too: the conditional operator! While, yes, value_or() exists as a convenience mechanism to avoid having to use the conditional operator, that doesn’t mean that you should just avoid it. It just means that you have to duplicate the name of the optional:

opt.value_or(val); // always a prvalue
opt ? *opt : val; // could potentially have any value category

If the way you got your optional is via some other expression, this isn’t really viable (you definitely don’t want to write foo() ? *foo() : val). But it does sometimes work, and it is a useful option to be aware of.

This kind of thing does come up, and it’s one of those odd language quirks that we can do the same thing two different ways, and usually one of them is just good enough for most of the things we want to do, but it’s just a little bit irritating that we can’t just have our cake and eat it too.

One way that we could be cake-gluttons would be to introduce a new operator. Swift, for instance, has something called a nil-coalescing operator, spelled ??. In C++ terms, we would say that a ?? b should have the same behavior as a ? *a : b except that a is evaluated exactly one time (and b as usual is evaluated at most one time). This would work any nullable types (not just optional<T> but also just T*, any of the smart pointers, etc.). Note that this is different from gcc’s Elvis impersonator extension a ?: b which means a ? a : b.

Now it’d be nice to have a simple expression that just does the right thing, all the time. But it’d also be nice to not have to keep adding operators to the language (it’s like Pringles — would we ever stop?). Maybe we need more expressivity in the language to be able to write functions that behave like this.

Either way, it’d especially be nice if I could live in a world where I just didn’t care about excessive copies and could use value_or() guilt-free. But this isn’t ‘Nam, there are rules.