App-level Developer on std::error Exceptions Proposal for C++. Part I. The Good

 
Author:  Follow: TwitterFacebook
Job Title:Sarcastic Architect
Hobbies:Thinking Aloud, Arguing with Managers, Annoying HRs,
Calling a Spade a Spade, Keeping Tongue in Cheek
 
 

DISCLAIMER #1: This post is about C++ proposal; it is not a part of the language, and won’t be implemented for a while. Moreover, ALL the details may change in any way, up to their exact opposites.

DISCLAIMER #2: I am NOT a member of WG21, so everything here is merely musings of somebody from outside. OTOH, I am sure that exactly because of being an outside view, these musings can be useful. 

DISCLAIMER #3: All the understanding below is just one rabbit’s opinion after reading proposal; it can happen to be wrong. If it is the case – please LMK, I will be happy to fix it.

Two Problems with Existing C++ Exceptions

Wtf hare:Probably, exceptions is one of C++98 features which are banned the most from modern real-world C++ projects.Traditionally, C++ exceptions are one of the controversial features of C++. Probably, it is one of C++98 features which are banned the most from modern real-world C++ projects.1 According to [DevSurvey0218],2 over 50% of the developers reported that exceptions are banned in at least some parts of their projects.

Apparently, there are two separate problems with existing C++ exceptions. The first problem is very obvious:

While existing C++ exceptions DO have (about)-zero runtime CPU cost when the exception is not fired, it comes at the cost of the path-when-the-exception-IS-fired being huge, and worse – being next-to-impossible to predict in advance.

According to [P0709R0], this is the reason why exceptions are banned in such projects as Joint Strike Fighter Air Vehicle C++ Coding Standards (JSF++), and the Mars Rover flight software.

In addition to this apparent performance problem, there is another issue with existing C++ exceptions, which is more of the philosophical nature (and which affects programming practices A LOT):

With existing exception model, we cannot see which functions are allowed to throw.

This leads us to the situation where we need either to (a) think that everything out there can throw (leading to very inefficient use of our brains to make everything out there exception-safe), or (b) forget about exception safety entirely (which actually happens way too often in real-world projects <sad-face />). This also happens to be one of major (though rarely-articulated as such) arguments for using error codes instead of exceptions – if we DO have to handle ALL the potential errors (such in OS kernel, military fighter, etc.) – it happens to be simpler to enforce a policy of “check ALL error codes” (with these models, ALL the functions of certain families DO return error codes, so it is not that difficult to enforce this policy), than to try tracking all the implicit exceptions (which are visible neither in the code nor in the function signatures,3 so we might need to dig into each and every function we’re calling to see if it may throw – AND pray that the function won’t change this behavior later without letting anybody know <ouch-and-double-ouch! />).

These two problems lead us to a situation when

No single error handling method is good enough for ALL the projects – which in turn leads to creation of C++ dialects, with some of the projects using exceptions, and some others using error codes

1 From what I’ve seen, overloaded << for text output is banned even more, but exceptions probably qualify as the second most-banned C++98 feature (banning RTTI is another contender but an effective RTTI ban is closely related to banning exceptions anyway)
2 Which survey IMNSHO has signs of heavy manipulation by certain groups leading to strange results in quite a few places, but we don’t have anything better anyway – and even if the numbers are off by 2x it is enough for our purposes
3 noexcept is still rarely encountered in the wild

 

Enter std::error Exceptions

This section proposes a solution —

not without trepidation, because I understand this touches an electrified rail.

— Herb Sutter, P0709R0 —

Very recently (that’s less than 3 weeks ago) a very audacious proposal was published by no less than Herb Sutter, under the name of [P0709R0].4

My humble understanding of the essence of the [P0709R0] goes as follows:

  • we get a new type std::error, which has the following properties:
    • it behaves pretty much as a good old POD.
    • it is VERY small (which allows to return it in CPU registers); current proposal says it mustn’t have size which is more than 2 pointers, suggesting two fields:
      • constexpr domain
      • integer payload
        • It seems that for certain domains (as the destructor of std::error doesn’t have to be trivial) we MAY be allowed to have a pointer-to-heap as std::error’s payload (which actually would mean that we can have whatever-information-we-may-want there).
  • we MAY declare some of our functions with a new keyword throws (not to be confused with an effectively-deprecated throw())
    • throws specification does NOT allow to specify which exceptions are thrown (all attempts to do this kind of things have failed badly in the real world, including C++ throw() which has never flew and Java’s throws which in real-world projects has degenerated to throws Exception denoting throwing any exception).
    • Judging hare:as soon as we have declared any function as the one which throws - THE ONLY type of exceptions which can come out of it, are std::error exceptionsas soon as we have declared any function as the one which throws – THE ONLY type of exceptions which can come out of itare std::error exceptions
      • If there an existing-C++-exception is thrown from within such a throws function, it is automagically converted to std:error (!!)

That’s pretty much the “core” of [P0709R0] (leaving optional parts of it for the time being) from our app-level perspective. Oh, and of course, for those functions marked with throws, the whole implementation of exception handling is changed drastically, making generated code MUCH more like our usual error-code-on-return – which means it becomes very straightforward and easy to predict, with no RTTI involved, etc. etc.

An example from [P0709R0]:

string f() throws {
  if (flip_a_coin()) throw arithmetic_error::something;
  return “xyzzy”s + “plover”; // any dynamic exception is translated to error
}
string g() throws { return f() + “plugh”; } // any dynamic exception is translated to error

int main() {
  try {
    auto result = g();
    cout << “success, result is: ” << result;
  }
  catch(error err) { // catch by value is fine
    cout << “failed, error is: ” << err.error();
  }
}

Note that as std::error is one single error type – it means that to handle different error codes we MAY have to write something along the following lines (again, example comes from [P0709R0]):

//SAFE_DIVIDE EXAMPLE - PO709R0
int safe_divide(int i, int j) throws {
  if (j == 0)
    throw arithmetic_errc::divide_by_zero;
  if (i == INT_MIN && j == -1)
    throw arithmetic_errc::integer_divide_overflows; 
  if (i % j != 0)
    throw arithmetic_errc::not_integer_division;
  else
    return i / j;
  }

double caller(double i, double j, double k) throws {
 return i + safe_divide(j, k);
}

int caller2(int i, int j) {
  try {
    return safe_divide(i, j);
  } catch(error e) {
    if (e == arithmetic_errc::divide_by_zero)
      return 0;
    if (e == arithmetic_errc::not_integer_division)
      return i / j; // ignore
    if (e == arithmetic_errc::integer_divide_overflows)
      return INT_MIN; 
    // Adding a new enum value “can” cause a compiler
   // warning here, forcing an update of the code (see Note). 
  }
} 

Note that using switch in caller2() is not possible and multiple ifs have to be used instead, and for a good reason too: comparison of std::error is intended to be semantic across domains, i.e. as [P0709R0] says“for example, “host unreachable” errors from different domains (e.g., Win32 and POSIX) compare equivalent to each other and to errc::host_unreachable which can be queried in portable code without being dependent on the platform-specific source error.”5


4 Boy, how do they manage to come up with such succinct names all the time?
5 IF they manage to pull it off – I would be extremely happy, but I don’t hold my breath until I see it working; technically it is simple, but agreeing on the codes can become a never-ending nightmare <sad-face />

 

Extension: Explicit Markup For Potential Failure Points

The “core” proposal as discussed above, does solve the first problem with existing C++ exceptions: being expensive and difficult-to-estimate on the exceptional execution path. However, the second problem mentioned above (lack of explicit notation for those-functions-allowed-to-throw) is still not addressed in the “core” of [P0709R0]. Still, it would be strange if Herb Sutter wouldn’t think about this problem – and he did, in a “proposed extension” of [P0709R0] (section 4.5).

His idea is to introduce “try expressions” and “try statements” along the following lines (once again, the example is from [P0709R0]):

string f() throws {
  if (flip_a_coin()) throw arithmetic_error::something;
    return try “xyzzy”s + “plover”; // can grep for exception paths
  try string s(“xyzzy”); // equivalent to above, just showing
  try return s + “plover”; // the statement form as well
}
string g() throws { return try f() + “plugh”; } // can grep for exception paths

It is all grand and dandy (and I LOVE the way it looks – the whole thing is BOTH self-documented, AND sufficiently non-intrusive), but given that such things tend to be perfectly useless unless enforced, we’re running into an all-important question:

How to enforce that ALL the calls to functions-which-may-throw are marked with ‘try’?

In this regard, I have good news and bad news:

  • good: with the syntax above, it is certainly enforceable (that is, IF we’d be able to write a new language)
  • bad: backward compatibility will be a major headache on this way. In general, two approaches are possible:
    • requiring ‘try’ on all potentially-throwing calls ONLY for new-functions-marked-with-throw
    • not enforcing it at compiler level at all, delegating all the responsibility to the Core Guidelines and relevant checkers.
    • In fact, the choice between these two options is not obvious (and actually seems to depend on “whether our project wants a smooth migration, or we have a new project where we want to ban all the existing exceptions in favor of std::error-based ones”; I’ll try to elaborate on it in Part II of this mini-series).

However, as soon as we ARE able to enforce these things – IMNSHO this extension DOES solve the second problem of existing C++ exceptions (that of the functions-throwing-errors being invisible to the coder).

My First Impression As an App-Level Developer

After my first and second glance at [P0709R0], I tend to like it. In particular, [P0709R0]:

  • keeps one single model for error handling
  • Hare thumb up:P0709 keeps most of the existing practices intact, so it is a fairly non-intrusive changekeeps most of the existing practices intact, so it is a fairly non-intrusive change to existing best practices.
    • in a sense, we can think of it as of “better implementation of the same exception idea”
  • it does improve performance where it matters (addressing one major problem of the existing exceptions)
  • it seems to allow to document which functions are allowed to throw (via ‘try expressions’) (to make it truly self-documented code we have to think about enforcing it – but this is a separate topic).

Actually, I like it enough to make a real-world usability experiment for this proposal on a medium-sized project (that is, IF I manage to convince other members of the team about it):

  • write our own class analogous of std::error
  • start using it across the project – using it exactly as it would be used though without throws. 
  • of course, we won’t be able to see all the issues of such an error handling, but it will allow revealing quite a few of them.
    • And of course, IF this proposal eventually flies, we’ll be one of the first projects who is able to benefit from improved exception performance <wide-smile />.

Comparing to Competing Proposals

Right above I said that I like the std::error exception proposal a.k.a.[P0709R0]. However, as soon as I look at competing proposals, I begin to love it. For example, the same SAVE_DIVIDE EXAMPLE as shown above, under competing [P0323R3] would look as follows:

//SAFE_DIVIDE EXAMPLE - P0323R3
expected<int, errc> safe_divide(int i, int j) {
  if (j == 0)
    return unexpected(arithmetic_errc::divide_by_zero);
  if (i == INT_MIN && j == -1)
    return unexpected(arithmetic_errc::integer_divide_overflows);
  if (i % j != 0)
    return unexpected(arithmetic_errc::not_integer_division);
  else
    return i / j;
}

expected<double, errc> caller(double i, double j, double k) {
  auto q = safe_divide(j, k);
  if (q) return i + *q;
    else return q;
}

int caller2(int i, int j) {
  auto e = safe_divide(i, j);
  if (!e) {
    switch (e.error().value()) {
      case arithmetic_errc::divide_by_zero:
        return 0;
      case arithmetic_errc::not_integer_division:
        return i / j; // ignore
      case arithmetic_errc::integer_divide_overflows:
        return INT_MIN;
      // No default: Adding a new enum value causes a compiler
      // warning here, forcing an update of the code.
    }
 }
 return *e;
}

Only caller2() (which is not really common use case to start with) can be seen as comparable-in-complexity to P0709; both safe_divide() and caller() are cluttered with barely-relevant stuff to the extent of being barely readable <sad-face />. Overall, my problems with the code from P0323 are that big that I cannot imagine myself voluntarily using it in any-project-where-I-can-use-exceptions; moreover, I am 99.99% sure that LOTS of app-level developers will share this point of view. In turn, it means that even in the very best case for such proposals, we’re speaking about the further split of the two incompatible error-handling paradigms, which is IMNSHO a Bad Thing(tm) per se. In constrast, P0709 aims to unify error handling around a widely-accepted (and IMO very convenient) exception paradigm, while solving both major issues of existing C++ exceptions.

To Be Continued…

Tired hare:It was the first part (“The Good”) of this mini-series on my impressions of std::error exception proposal. The second part (“The Controversial”) is scheduled to come in a week, so stay tuned! <wink />

Don't like this post? Comment↯ below. You do?! Please share: ...on LinkedIn...on Reddit...on Twitter...on Facebook

[+]References

Acknowledgement

Cartoons by Sergey GordeevIRL from Gordeev Animation Graphics, Prague.

Join our mailing list:

Comments

  1. Arioch says

    Enforcing try-expr everywhere would be just a noise for the sake of noise.

    Perhaps it could be seen as kind from a safe/unsafe code domains point of view.
    Which situations exactly should be considered dangerous and required to be expkicitly vetted by a developer?

    Let’s consider a function call from another function.

    If both functions are exceptions-aware then we are good already and no extra vetting needed.

    If both functions are exceptions-unaware then we just have a legacy code with no need to inject the noise into every second statement.

    If we descend from aware fn to unaware fn then again no vetting is called for.

    Now, if we call exceptions-aware function from an exception unaware one – then we must explicitly tell the compiler we know here what we are doing, and here is that 1 of 4 case when try-calling can be required.

    But then should it be made into compiler magic?
    Can it be implemented as some template over functions?

    Now, with expressions. We may simply consider expressions as specific king of inline function call and apply rules from above. Especially as many expressions would have function calls in them.

    But perhaps the try-expression can modify how compiler generates code?

    int i,j,k;
    i = MAX_INT / 2; j =i+i;

    k = i+j; // just a casual call, integer wrap-around, developer should had coded explicit checks.

    k = try(i+j); // not only permitting compiler to throw, but actually asling it to check this and that and actually to throw the integer overflow exception

    try k = i+j; // try without catch and curlies, maybe a third option to signal some yet another option

    • "No Bugs" Hare says

      The very similar line of argument can also be applied to const, cannot it? And still, const (while having pretty much zero impact on generated code) is widely recognized to be useful (I’d say it is VERY useful, especially to keep million-LoC codebases refactorable).

      > If both functions are exceptions-aware then we are good already and no extra vetting needed.

      Not really. Functions may be SUPPOSED to be exceptions-aware, but without explicit markers it is more difficult to ensure this exception-awareness, and worse – exception correctness of the caller might easily change – merely because callee has changed its signature and started to throw – without developer of the caller noticing it. IMO, this is a situation which is VERY similar to the const – it IS possible to write correct programs without const, but – const does help both to document things, and to reduce maintenance costs (as some inadvertent API changes are rejected by the compiler and force us to think a bit more at that point).

      > k = try(i+j); // not only permitting compiler to throw, but actually asling it to check this and that and actually to throw the integer overflow exception

      I am not sure but I am afraid it would go against principles on which C++ is built (that expressions are self-contained, and that exception result doesn’t depend on how-exception-is-used).

      ———

      That being said, I can say that I’d argue AGAINST making missing-try-in-functions-declared-as-throws a compile-time ERROR – based on a mere observation that VAST MAJORITY of the existing app-level code is NOT exception-safe (at least not beyond RAII which is only weak exception safety, and even RAII doesn’t protect well in case of certain failures such as heap failures). As a result, IMO detection of missing-try-in-functions-declared-as-throws should be either delegated to a checking tool, or (potentially better, as it would stimulate thinking about exception safety – thing which is badly missing in lots of projects, ) should become a compiler warning (if I won’t need it in my project – I will simply disable the warning project-wide). More on it in upcoming Part II of this mini-series.

Leave a Reply

Your email address will not be published. Required fields are marked *