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 statement(?).
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() {
   if(Ok)
      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.

--
Dmitry
Sent from gmail