Hi all,

I understand it is slightly off-topic, but I really wanted to thank Barry for his elegant and beautiful idea, I have successfully implemented it and the code below feels just other-worldly:
I can now use it in this way:
TCoResult<int, std::string> CreateSocket() {
   static int Counter = 1;
   if(++Counter % 2 == 0)
      co_return "Failed to CreateSocket: SysErr: EINVAL Invalid argument";
   co_return 42;
}
TCoResult<int, std::string> OpenSocket() {
   const int Socket = 2 + co_await CreateSocket().OrNestAndReturn();
   co_return Socket * 2;
}
TCoResult<int, std::string> ConnectSocket() {
   co_return co_await OpenSocket().OrNestAndReturn();
}
TCoResult<std::string, std::string> ReadSettings() {
   co_await ConnectSocket().OrNestAndReturn();
   co_return OkRes("Here is our settings");
}
int main(int, char *[]) {
   std::cout << ReadSettings() << std::endl << std::endl;
   std::cout << "==== Second attempt ====" << std::endl;
   std::cout << ReadSettings() << std::endl << std::endl;
   return 0;
   // Expected output:
   // Err(Failed to ReadSettings @ Line:20: Failed to ConnectSocket @ Line:17: Failed to OpenSocket @ Line:13: Failed to CreateSocket: SysErr: EINVAL Invalid argument)
   // ==== Second attempt ====
   // Ok(Here is our settings)
}


In case you are interested here is the godbolt link with complete implementation to play around: https://godbolt.org/z/8hxhttps://godbolt.org/z/8hxKTWKTW
and here is repo.

PS OrNestAndReturn() uses source_location to generate messages with line numbers and function names.

On Mon, Sep 14, 2020 at 8:04 PM Barry Revzin <barry.revzin@gmail.com> wrote:


On Mon, Sep 14, 2020 at 1:44 PM Dmitry Dmitry via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
TL;DR: What do you think about creating a new flavour of control-flow statements (in particular "return") that could return from outer/parent scope?

In the thread "Initialisers in ternary operators" we had this example:
std::optional<std::string> FindUsersCity(bool non_default) {
   std::optional<ContactsServer> contacts = GetOrOpenContactsServerConnection();
   if(!contacts)
      return std::nullopt;
   std::optional<UserId> uid = contacts->GetUserId();
   if(!uid)
      return std::nullopt;
   std::optional<GeoServer> geo = GetOrOpenGeoServerConnection();
   if(!geo)
      return std::nullopt;
   std::optional<Location> uloc = geo->GetLocation(*uid);
   if(!uloc)
      return std::nullopt;
   return uloc->GetCityName();
}


It seems, that there are many situations with the following pattern:
  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/Outcome/Expected 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 consider other control-flow statements.

Without going into implementation details, what do you think about the idea (on a conceptual level) of being able to describe this pattern somewhere somehow?

If conceptually it is fine, 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 outer/parent scope?
If you wish, this new flavour can be viewed as a middle ground between (1) regular return's (which often requires boilerplate for handling the pattern above) and (2) exceptions (which are too non-local and implicit and can "jump" at an arbitrary place).

The code above would look like this:
std::optional<std::string> FindUsersCity() {
   std::optional<ContactsServer> contacts = GetOrOpenContactsServerConnection();
   std::optional<UserId>         uid      = contacts.GetOrReturnNullOpt()->GetUserId();
   std::optional<GeoServer>      geo      = GetOrOpenGeoServerConnection();
   std::optional<Location>       uloc     = geo.GetOrReturnNullOpt()->GetLocation(*uid);
   return uloc.GetOrReturnNullOpt()->GetCityName();
}

where GetOrReturnNullOpt() is a method that (1) either returns a value if std::optional is not empty, (2) or returns from FindUsersCity() if std::optional is empty.


I think it could solve
  • Initial problem mentioned in "Initialisers in ternary operators": we could have something like this: return Cont.find(42).GetOrReturn(0).
  • The problem of using optional without boilerplate (see above).
  • The problem of Error-handling via Result<> without boilerplate, which has been a constant source of pain and complaints and holy wars between exceptions and return-values for error handling.
  • It also eliminates one more reason to exist for macros (that is how people are solving it right now: 1, 2).

Any thoughts?


--
Dmitry

As I said in https://lists.isocpp.org/std-proposals/2020/09/1811.php, this already exists in C++20 in the form of coroutines... you just need to opt in support for optional<T> in the only sensible way and then you can write:

std::optional<std::string> FindUsersCity(bool non_default) {
    ConstactsSeriver cs = co_await GetOrOpenContatsServerConnection();
    UserId uid = co_await cs.GetUserId();
    GeoServer gs = co_await GetOrOpenGeoServerConnection();
    Location uloc = co_await gw.GetLocationId(uid);
    return uloc.GetCityName();
}

co_await here is effectively syntax sugar for and_then() (in the same way in Haskell do-notation is syntax sugar for bind). It's just that the continuation gets written as the next statement in the function rather than the body of a lambda that gets passed as an argument.

Barry


--
Dmitry
Sent from gmail