1. 9
  1. 9

    He raises the dynamic allocation problem of exceptions, but I have another problem with exceptions that I don’t really see treated a lot: Complete interface obfuscation. Suppose you include a library header. Suppose that library allows you to call void foo();. Can that function fail? If so, what exceptions does it fail with, under what circumstances? If you’re lucky, the documentation is good. Often, the documentation doesn’t even exhaustively list possible exceptions. Often, I have to look through the code and search for throw statements. As far as I know, there isn’t even good tooling to run over a code base to find out what exceptions a function may throw (please tell me if you know one). How is that ever acceptable? I want to know how and why my API calls can fail.

    Contrast this with the Rust approach. We immediately see: fn foo() cannot return any errors* and fn foo() -> Result<T, E> will either give me a T on success, or an error description E on failure. If that mechanism makes error handling harder, that’s just because with exceptions, you didn’t even really handle them. I’ve actually worked on numerous C++ projects where the exception madness caused very real problems. Ever since, I completely stopped using them and started wrapping libraries that use them such that exceptions are turned into error codes. This also forces me to find out how the API calls can fail.

    * Okay, there’s still the panic mechanism, but this is only for rare and very severe errors. Avoid it whenever possible.

    1. 10

      there’s still the panic mechanism, but this is only for rare and very severe errors

      Panic in Rust is for signaling that a bug in the program has been detected. It is not appropriate for expected failure modes in a correct program. For rare or very severe, abort would be better.

      1. 6

        This is the checked vs unchecked exception choice. C++ tried to support both for a while, for example:

        void doesNotThrow() throw();
        void throwsForOrBar() throw(Foo, Bar);
        void mightThrowAnything();

        There are a few problems with this approach. The first is that adding a new error result changes the signature of a function. This is not specific to C++ but it generally means that you rely on subtype relationships to extend the set of things a function can return. In the worst case in Java you get things that can throw Exception - great, it will throw some subclass of a generic exception class and I’m expected to down-cast it to the right thing once I get it and figure out what to do with it. Thanks. Oh, and this was worse in C++ because exception specifiers were not part of the type system so you could store all of the above in a void(*)() variable and have absolutely no idea what exceptions would be thrown.

        The second problem is that C++ decided to dynamically enforce these things. This meant that your unwind logic had to check exception specs on the way up the stack and a function with throw() would get unwind tables generated to guarantee that exceptions didn’t propagate through it (and would call std::unexpected in these cases). This was very painful for the unwinder and for binary size.

        C++17 removed exception specifiers and made noexcept part of the type system. This means at least that you know whether exceptions can or can’t be thrown but you still rely on documentation to know which exceptions might be thrown.

        1. 1

          Suppose that library allows you to call void foo();. Can that function fail?

          The noexcept keyword helps with that, but the library implementor has to add it. And there’s no static checking that I know of to verify that a noexcept function’s implementation doesn’t fail to catch any exceptions (in which case the program will abort rather than allow the exception to propagate.)

          My opinion is that in C++, exceptions are deeply problematic, but better than current alternatives. Error codes are toxic for good API design and don’t work with constructors or operators. There are some decent library implementations of Rust/Swift/etc. Result types, but I haven’t tried using them in any serious codebase yet.

          I’ve seen a C++ WG proposal for overhauled exception handling that works very much like Swift under the hood, and it looks great, but I don’t know if that’s going into C++23.

        2. 3

          Unfortunately, this heap must be able to grow dynamically because there exists no meaningful upper bound for allocations on behalf of the C++ runtime. E.g., in a multi-threaded application, multiple exceptions may be in flight at the same time.

          I’m not entirely convinced by this. Obviously true in theory, unclear in practice. A scheme which would work for - say - a max of 100 simultaneous exceptions would be to allocate a (fairly small) buffer and take free slots in that for the exception header. What %age of use cases exceed 100? How about 1000? Does a working assumption of “no more than X OS threads per physical processor” help set a meaningful bound?

          This has the disadvantage in that it fails (how? - likely a hard stop with an informative error) if you violate this bound. But the advantage in that you get to carry on doing what you’re doing code-wise and not worry about this.

          It used to be common practice to have fixed bounds in the kernel, and recompile with different bounds for different workloads. This isn’t ideal, but it can be pragmatic, if it is easy to put in place a bound which is unlikely to be hit.

          1. 3

            Part of the problem is that this is per thread, so it can add up to quite a large amount of space. The other part of the problem is that C++ allows you to throw value types, which can be arbitrarily large. With the Windows unwinder, this has very little overhead because they’re allocated on the stack and then the unwinder runs as ‘funclets’ (functions that have access to the stack frame of another function) on top of the stack. This means that all cleanup code and all nested exceptions (and all unwinder state) are all on the stack. In the Itanium / DWARF ABI, most of the state is heap allocated and so the exception object is a variable-sized allocation that holds the unwinder state and the thrown object. Exception objects are usually small, but in theory you could throw a 1 MiB object, by value, and have several of them in flight at a time. C++11 made this slightly worse by providing an accessor to get the current in-flight exception so that you can forward it to another thread, which means that you have to be able to allocate it on the heap, even if your fast path stores it only on the stack.

          2. 2

            Love the functionality, dislike the verbosity of the syntax.

            I tried recently to come up with something like Swift’s if let and guard…else statements, for working with C++ options and variants. These have the behavior described in the article, that you only have access to the result inside a scope where the result is known to exist. But even using macros I couldn’t figure out how to do it.

            1. 3

              Sounds like a fun exercise!

              This pattern is already pretty close to if let:

              if (Cat* c = dynamic_cast<Cat*>(&animal)) {...}

              which also works with optionals and variants:

              if (std::string* s = std::get_if<std::string>(&json)) {...}
              if (optional<int> limit = settings.memLimit) {...}

              One problem though, is it doesn’t unwrap the type. limit is an optional<int> which is never empty, so I’d rather have an int.

              You can fix that by binding the name after doing the check:

              #define if_let(ID, Expr) \
                  if (auto&& tmp = (Expr)) \
                  if (auto&& ID = *tmp; true)
              if_let(limit, settings.memLimit) {...}
              if_let(s, std::get_if<std::string>(&json)) {...}

              This helper makes the variant one a little nicer too:

              template<class T>
              auto* as(auto&& variant_like) {
                  return std::get_if<T>(&variant_like);
              if_let(s, as<std::string>(json)) {...}

              guard ... else seems harder though. My first thought is something like:

              #define guard(ID, Expr, ...) \
                  auto&& tmp = (Expr); \
                  if (tmp) {} else { {__VA_ARGS__;} } \
                  auto&& ID = *tmp;
              guard(s, as<std::string>(json), {
                // s not in scope here
                return some_error;
              // can use s here

              but I don’t want the else clause to fall through. And I want it checked at compile time.

              Here’s what I came up with:

                  if (tmp) {} else { return []{__VA_ARGS__;}(); } \

              Returning from inside the lambda is sort of equivalent to returning from the containing function. If you forget to return, it’s either some kind of type error, or it implicitly returns void. You also can’t use break or continue any more.

              Ideally I’d want some kind of static_assert_unreachable() primitive.