C++ Logo


Advanced search

Re: Initialisers in ternary operators

From: Dmitry Dmitry <dimanne_at_[hidden]>
Date: Sun, 13 Sep 2020 09:48:53 +0100
> But there was one strong outlier, and it looks like this:
> std::optional<std::string> oldFindUsersCity(bool non_default) {
> std::optional<UserId> uid = UserId{};
> if (non_default) {
> uid = GetUserId();
> if (!uid) return nullopt;
> }
> std::optional<Location> uloc = uid->GetLocation();
> if (!uloc) return nullopt;
> return uloc->GetCityName();
> }
> Those if/return pairs make the code really ugly really fast, so people
> made macros:
> std::optional<string> FindUsersCity(bool non_default) {
> UserId uid;
> if (non_default) ASSIGN_OR_RETURN(uid, GetUserId());
> ASSIGN_OR_RETURN(Location uloc, uid.GetLocation());
> return uloc.GetCityName();
> }

While reading this, I realised that it reminds me very much of another area
of day-to-day programming: error handling via Result/Expected/Outcome,
which we do a lot. (Note, however, that it is not entirely true, because
*sometimes* before returning an error you want to capture some context, add
some explanation and so on..., which means it is not *just* return).

Nevertheless, it seems, a pattern emerges. In all the cases,

   1. there is a type
   2. that defines some invalid state (in case of iterator the state is
   "being equal to end()", in case of Result the state is "representing an
   error", in case of optional it is "nullopt", and so on...)
   3. attempts of using which in the invalid state are supposed to "return"
   (from the scope where it is used). We can take a step back and say that
   instead of just "return" we, *perhaps*, can allow any other control

Without going into implementation details, what do you think about the
conceptual idea of being able to describe this pattern somewhere somehow?
If conceptually it is not very awful, then what do you think about
implementing the idea by creating another flavour of return statements that
can return not only from its own scope, but also from parent scope? If you
wish, this new flavour can be viewed as a counterpart of couritine control
flow statement (co_yield), which expects control-flow to continue execution
after co_yield (and therefore does not "entirely" leave the scope, while
the new flavour will leave *parent* scope as well).

The code would look like:
// new implementation of Result's GetOk that can return from *outer* scope:
class Result {
T GetOkOrReturn() {
      return OkValue;
   return_from_parent_scope; // <-- new flavour of return

// usage
void DoSomething() {
   Result<int, std::string> uidOrErr = GetUid();
   const int uid = uidOrErr.GetOkOrReturn(); // <-- returns from
*DoSomething* in case of error.

A more elaborate example will need to mention non-void return types as
well, but I think it gives an idea.

It seems that it solves all mentioned problems:

   - iterators being equal to end,
   - using optional without boilerplate,
   - error-handling via Result<> without boilerplate, which has been a
   constant source of pain and complaints,
   - it *also* eliminates one more reason to exist for macros.

*Sent from gmail*

Received on 2020-09-13 03:49:43