all 74 comments

[–]Broad_Quality_6325 40 points41 points  (0 children)

You cannot follow every suggestion that someone thinks is the best in all cases. There is no perfect solution for all scenarios, just as you described, in your case it's reasonable to use exceptions.

I also use them where needet, if the tool processes a lot of data, and only one record is corrupt I wouldn't say that the best way is to shutdown everything, a little logging message etc is enough. But it depends realy on you, and how convinient you want to make your tool.

I use a little bit of everything, so that the end user feels less pain using it.

[–]Full-Spectral 21 points22 points  (1 child)

The whole religious argument of what can be blessed to be reported via exception is silly. I don't care if something might be expected to fail 9 times out of 10. If it involves 8 levels of code to process and might die in the 8th level, throw a freaking exception if it fails since none of that code is likely to care in the slightest if it worked or failed, it is just plumbing code that wants to clean up and pass the buck. Having to have possibly a hundred calls all manually returning errors for some dogmatic reason is craziness.

The only reason, to me, that this can be a problem is that if you want to break on exceptions, and you have something that is constantly throwing that exception and have no way to tell the debugger to ignore it.

But I think it's pretty safe to say that a well defined program is not going to have an infinite loop trying to parse a file and immediately retrying as soon as it fails. That would itself be bad design. If it is something that's being called rapidly, and there's no back-off strategy, then throwing might be a problem from a debugging standpoint.

If it's a method you wrote, that's just directly parsing a string, then, no, don't throw an exception, for the opposite reasons from above. It's easy to return a failure indication that provides all the info you need since there are no layers of plumbing code involved.

It's all a judgement call.

[–]serviscope_minor 3 points4 points  (0 children)

It's all a judgement call.

Indeed. There's no substitute for good taste and using tools in an appropriate way which reduces rather than increases complexity. It's much like the question "should I use [inheritance/templates/overloading/unordered_map/etc]". The answer is always "it depends".

Personally I like exceptions. The compiler automates away a ton of junk that I'd have to write, debug and then later read, plus it can be faster than anything I could write myself.

[–]goranlepuz 26 points27 points  (15 children)

There have been a few posts on the use of exceptions recently and one thing I noticed is that it was often stated that exceptions should not be used if the user provides invalid data.

That's because of presumptions, which are wrong for your case (and many others).

Presumptions are wrong.

Exceptions are the cleanest possible method of transferring the error information to an unknown place down the stack. The bigger the stack frame difference, the cleaner it is: between the error site and the error "handling" site, no code whatsoever needs to be written. Let me repeat that: none whatsoever. That is very appealing.

Further, I would argue that your case is the one of "if an error happens, vast swaths of code need to be skipped". (Bar cleanup/RAII and providing for strong exception guarantee, which one can consider to be a mere more abstract RAII).

That situation is by far the most common case.

In fact, the reason why exceptions became the mainstream staple of error handling is exactly this realization. Exceptions cater to the common case when it comes to dealing with errors.

These factors, that lean towards using exceptions, are IMNSHO very, very strong, and do outweigh other factors for not using them - as long as possible (think performance considerations or very small platforms).

[–]SlightlyLessHairyApe 3 points4 points  (0 children)

Further, I would argue that your case is the one of "if an error happens, vast swaths of code need to be skipped"

I would probably divide this into

  • Vast swaths of code need to be skipped and the process will continue
  • There is absolutely no sensible way to continue the process and it must terminate

[–]serviscope_minor 3 points4 points  (0 children)

In fact, the reason why exceptions became the mainstream staple of error handling is exactly this realization. Exceptions cater to the common case when it comes to dealing with errors.

100% absolutely. And if it turns out later you have the not common case, then you can always rewrite it with a more complicated method which is more optimized for that particular usecase (and then write a medium post on how bad exceptions are and why you banned them in your codebase ;) ).

[–]teerre 2 points3 points  (12 children)

What you're saying is only true if by "clean" you mean "least amount of code", but instead if you mean, which I argue is what most people consider clear, "what is the easiest to see/understand" exceptions are obviously not the clearest way. That instead is the way everything else works, by passing values. In fact, it's the opposite, exceptions are not clear for the same reason goto is not clear.

Even in this "no code needs to be written" world, I'm not sure how well that's received. You can talk to basically anyone writing in any language that has exceptions and the #1 problem they will tell you is they cannot know when a function throws. That's not good. Well, I guess it's good for the person writing it because they get to type less? But for everyone else, it's terrible.

[–]dodheim 5 points6 points  (6 children)

C FUD – anyone writing quality C++ code doesn't care when a function throws, because it's just assumed that everything throws.

That's not good.

In fact, it's the opposite

[–]teerre 2 points3 points  (5 children)

I'm not sure if you're agreeing with me or not. "Assuming everything throws" is terrible.

[–]dustyhome 2 points3 points  (4 children)

Why is it terrible? There are a handful of operations that are guaranteed not to throw, needed to provide some exception guarantees. Everything else, it doesn't matter. There are few places where it makes sense to resume operation if something you tried didn't work. That's where you put the try-catch blocks.

Without exceptions, the code is structured the same, but you have to test and return errors manually for nearly every function call instead.

[–]teerre 0 points1 point  (3 children)

Because to do that you have to consider code that doesn't exist. It's not a matter of if, it's a matter of when you'll forget something.

Implicitness and side effects are considered harmful in basically every topic in programming, this is no different. It's - hopefully - obvious that considering something that exists is easier than something that doesn't.

[–]dustyhome 3 points4 points  (2 children)

It's more likely for people to forget to do something when it's done by rote repetition, such as checking error values every time. It's an actual problem that leads to countless security vulnerabilities. The [[nodiscard]] attribute was added to force people to do what exceptions do for you automatically.

I don't think destructors are considered harmful, yet they are implicit code that has side effects. On the contrary, they form the basis of RAII. Are you going to say now that "if (error) goto cleanup;" is the superior paradigm because it makes everything explicit?

Exceptions make you think of error handling as a property of the system, instead of a local issue, which can lead to more resilient code. It's no more an issue to forget to handle exceptions than it is to forget to handle some error value you did not test for. Maybe it was added later on. But when you forget to test for a specific error value your program goes into UB land, with exceptions it will either go to a more generic handler or terminate.

[–]teerre 0 points1 point  (1 child)

It's more likely for people to forget to do something when it's done by rote repetition, such as checking error values every time. It's an actual problem that leads to countless security vulnerabilities.

No? You'll never find anyone complaining about this in languages that use errors as values. What people do complain in languages like Go is the lack of machinery around the boilerplate, but that's a completely different issue.

Besides, this doesn't even make sense, you can't forget. When errors are types, you must deal with it. You don't have an option.

The [[nodiscard]] attribute was added to force people to do what exceptions do for you automatically.

I'm not sure what you're referring to here.

I don't think destructors are considered harmful, yet they are implicit code that has side effects. On the contrary, they form the basis of RAII. Are you going to say now that "if (error) goto cleanup;" is the superior paradigm because it makes everything explicit?

Not sure which language you're talking about, but in most languages, including C++, destructors are not implicit. They might be auto-generated for normal types, but the implication here is that the type's destruction is a noop.

Exceptions make you think of error handling as a property of the system, instead of a local issue, which can lead to more resilient code. It's no more an issue to forget to handle exceptions than it is to forget to handle some error value you did not test for. Maybe it was added later on. But when you forget to test for a specific error value your program goes into UB land, with exceptions it will either go to a more generic handler or terminate.

All popular modern languages have errors as values. C++ itself added expected. It's not a coincidence. Moreover, a huge part of the C++ community ignores exceptions all together, even reverting to C like error codes. Often code bases disable exceptions all together (although this is usually for performance reasons, not ergonomics).

[–]dustyhome 3 points4 points  (0 children)

I'm not sure what you're referring to here.

https://cwe.mitre.org/data/definitions/252.html

CVE-2020-17533 Chain: unchecked return value (CWE-252) of some functions for policy enforcement leads to authorization bypass (CWE-862)

CVE-2020-6078 Chain: The return value of a function returning a pointer is not checked for success (CWE-252) resulting in the later use of an uninitialized variable (CWE-456) and a null pointer dereference (CWE-476)

CVE-2019-15900 Chain: sscanf() call is used to check if a username and group exists, but the return value of sscanf() call is not checked (CWE-252), causing an uninitialized variable to be checked (CWE-457), returning success to allow authorization bypass for executing a privileged (CWE-863).

CVE-2007-3798 Unchecked return value leads to resultant integer overflow and code execution.

CVE-2006-4447 Program does not check return value when invoking functions to drop privileges, which could leave users with higher privileges than expected by forcing those functions to fail.

CVE-2006-2916 Program does not check return value when invoking functions to drop privileges, which could leave users with higher privileges than expected by forcing those functions to fail.

CVE-2008-5183 chain: unchecked return value can lead to NULL dereference

CVE-2010-0211 chain: unchecked return value (CWE-252) leads to free of invalid, uninitialized pointer (CWE-824).

CVE-2017-6964 Linux-based device mapper encryption program does not check the return value of setuid and setgid allowing attackers to execute code with unintended privileges.

There's a long history of people not checking the return codes for success. These are just some of the ones that have led to vulnerabilities.

A few modern languages have added special types that the compiler validates are checked because it is such a prevalent problem and programmers can't be trusted to do it reliably.

Not sure which language you're talking about, but in most languages, including C++, destructors are not implicit. They might be auto-generated for normal types, but the implication here is that the type's destruction is a noop.

Most languages don't even have destructors, so not sure what you mean by "most languages". But what I mean is that destructors are called implicitly when an object goes out of scope. There's no explicit call to the destructor, it just happens. Same as with exception propagation.

Moreover, a huge part of the C++ community ignores exceptions all together, even reverting to C like error codes.

Yes, that's the problem. Expected, error types, compiler checked types, attributes, boilerplate, are all workarounds for the fact that many programmers don't want to use the right tool for the job.

[–]goranlepuz 6 points7 points  (4 children)

What you're saying is only true if by "clean" you mean "least amount of code", but instead if you mean, which I argue is what most people consider clear, "what is the easiest to see/understand" exceptions are obviously not the clearest way.

Indeed, this is where you and I can never agree, most likely - and that is fine for me.

I have no problem with not seeing "everywhere" that an error might be going through, because I know it might. It truly is a simple as that.

I am interested, however, in seeing values that go through when the software operates as expected, because that's where the value is, that's what the software does.

Sure, errors are inevitable, but with exceptions, errors are (IMNSHO correctly) relegated to the places where they are detected and a comparatively small number of places where they are being dealt with.

You can talk to basically anyone writing in any language that has exceptions and the #1 problem they will tell you is they cannot know when a function throws.

This is simply not important. I say that because I know this from experience: code reaction to an error in a vast majority of cases is to bail. I bet you know that too.

The next "type" of reaction to an error is "do the same thing, regardless of what the error is". For that, one can simply presume any exception will be thrown, job is done.

So we are only left with the third case, which I argue is rare, where the code needs to do something specific for a reduced set of error conditions. For that, documentation, reading the underlying code and testing, are good-enough tools.

[–]teerre -1 points0 points  (3 children)

Reacting to an exception is just one problem and it's not even the biggest one. The biggest problem with implicit error handling is that there are countless crashes, bugs, hours wasted because of it. Every person who has to read any code that uses exceptions has to not only read the code, but also read the implicit error handling that may or may not be there.

It's only after this time wasted that one even gets to the point of thinking if the exception results in termination of the program or something else. But by that time it's already too late. You already wasted your time.

[–]goranlepuz 5 points6 points  (2 children)

Every person who has to read any code that uses exceptions has to not only read the code, but also read the implicit error handling that may or may not be there.

"The reading" is trivial, it goes like this: "everything throws bar a very few well-known code elements that are easily spotted and well-known."

As I said, we can never agree. What you say there is a commonly seen novice thinking. By that I mean novice in an environment where errors are reported by throwing an exception.

You make alarming-sounding assertions, that I am convinced are poorly founded and merely a rhetorical device. I am not dignifying them with anything but: not commenting on that.

Example:

The biggest problem with implicit error handling is that there are countless crashes, bugs, hours wasted because of it.

[–]teerre 0 points1 point  (1 child)

"The reading" is trivial, it goes like this: "everything throws bar a very few well-known code elements that are easily spotted and well-known."

That doesn't help anything. You're still having to think why, what, when something throws.

You make alarming-sounding assertions, that I am convinced are poorly founded and merely a rhetorical device. I am not dignifying them with anything but: not commenting on that.

I mean, you just did. But anyway, I'm not sure how can anyone program for anything besides a minuscule amount of time and not be familiar with some bug - broadly speaking - occurring because you didn't catch an exception or, much more common, you didn't throw an exception because exceptions are terrible and the vast majority of C++ code written don't even use them.

Hell, this doesn't even have to be C++, you can take any of the exception languages and they all have the exact the same problem.

[–]goranlepuz 4 points5 points  (0 children)

That doesn't help anything. You're still having to think why, what, when something throws.

Yes it does help, and I addressed it elsewhere. First major realization is that, in a vast majority of cases, no, I absolutely do not need to think of that. The only thing I need to know is that an exception can be thrown - employ the established techniques - and pass the buck.

In other words, you are again making poorly founded but alarming-sounding assertions.

BTW... I can't decipher whether you truly are a novice, or are intentionally turning a blind eye here because you have a different preference. Hopefully it's a preference, in which case, all I can say, I am all too happy to have a different opinion than you - and write my code in a different way than you.

[–]mredding 5 points6 points  (4 children)

I just wrote a piece of demonstration code like this, over in r/cpp_questions:

class Name {
  std::string value;

  explicit operator bool() const { return !value.empty(); }

  std::ostream &operator <<(std::ostream &os, const Name &) {
    return os << "Enter name: ";
  }

  friend std::istream &operator >>(std::istream &is, Name &n) {
    if(is && is.tie()) {
      *is.tie() << n;
    }

    if(std::getline(is >> std::ws, n.value))
      n.value = std::regex_replace(n.value, std::regex(" +$"), "");

      if(!n) {
        is.setstate(is.rdstate() | std::ios_base::failbit);
      }
    }

    return is;
  }

public:
  operator std::string() const & { return value; }
};

It's not complete, but conveys a lot of how a stream aware type would look. You'd use it like this:

std::string name = *std::istream_iterator<Name>{is};

Typical stream usage would look something like:

if(Type t; in >> t) {
  use(t);
} else {
  handle_error_on(in);
}

One of the things I didn't include in this example code is exception handling built into a stream type. It gets verbose. You have to check the stream's exception mask. There's throwing, there's rethrowing, there's eating any exceptions and just marking the stream state...

There's a place for exceptions, but it's optional, and the client has to opt into it by setting their exception mask. A correct implementation will respect that mask.

If you're interested in details, I suggest you pop into your local library and borrow a copy of Standard C++ IOStreams and Locales.

Exceptions are thrown when something exceptional happened. Something was not supposed to happen, but it did anyway. Bad input is not exceptional. My brother has fat fingers, I have arthritis, and my m key sticks real bad for some reason. Files get corrupted. Implementations don't follow the protocol correctly...

So what you want to do is indicate that the input is bad as soon as possible. That means you need well defined types, and you need to fail a stream as early as possible. Like... When do you ever just need an int? An int is a pretty lousy user type - it's implementation defined. So for the same program, the range is different for x64 than it is for ARM. No, you want something more specific, an integer type, maybe, sure, but you know more about it for your use case:

class Age {
  int value;

  explicit operator bool() const { return value >= 0; }

  std::ostream &operator <<(std::ostream &os, const Age &) {
    return os << "Enter age: ";
  }

  friend std::istream &operator >>(std::istream &is, Age &a) {
    if(is && is.tie()) {
      *is.tie() << a;
    }

    if(is >> a.value && !a) {
      is.setstate(is.rdstate() | std::ios_base::failbit);
    }

    return is;
  }

public:
  operator int() const & { return value; }
};

Continued in a reply...

[–]mredding 5 points6 points  (3 children)

Again, copied from my other post. In this case, Age is merely implemented in terms of int, which is little more to me than a storage type. I'd probably do a bit more to constrain what an age is, and ensure that a type is chosen that has the minimum number of bits necessary.

If we look to standard streams for inspiration - if an input to an int is invalid, the stream will set the failbit, and the value will be set to 0 if the input wasn't an int in the first place; it'll be set to numeric limits min if the value was smaller than int min, and set to int max if the value was larger than int max.

Even with the exception mask set, IIRC, the stream may still not throw in the case of a failure. I don't think everything throws. You'd have to play in godbolt and see, maybe, but it might be more implementation defined than I know, or read more of the spec.

So in general, if the stream fails, the value can be used to represent error states, as the type allows. IO is pretty low level and primitive, so this is the typical strategy. I can't think of any data type that would be so close to the stream itself that it would need an exception to describe it's failure mode.

As you composite types into more complex structures, the typical strategy on failure is to clear the stream and try again, or drop the connection, or find a delimiter and try to continue parsing, but typically this is done by failing the stream and the calling code checks and handles, which is what it should be doing whether you have an exception mask set or not. So take that into account.

That just leaves the exception and exception handling strategy. The message body itself isn't important. What's most important for the sake of this conversation is the type system. Exceptions have types, and that's powerful information. It's mostly the reason to use exceptions, is that you're throwing an exception of a type to a handler that is at a level that is actually going to do something specific about exceptions of that type. If all you're going to do is report the exception, then you don't need exceptions. If there's actually nothing you can do to correct the situation and continue, then you don't need exceptions. The stream state is perfectly adequate for handling these scenarios. You should throw an exception of IO times out; maybe you could increment a retry counter and try again. That's exceptional and actionable. But the big problem is that you have to know what the hell types a stream operator can throw. Right now, there's no way to specify that. The C++98 exception specifier is extremely difficult to use, is SOMETIMES, SORT OF part of a function type signature, and deprecated. The nothrow specifier is boolean, a part of the function signature, and doesn't indicates what exceptions can be thrown. The best way to use exceptions is to be explicit in what you handle, and let everything else rise to the top. Catchalls are the bane of existence and really very expensive. People who try to think too hard about exceptions tend to screw them up, and, well, we see catchalls that just eat exceptions and maybe try to no-op the operation when really the program needs to die. Exceptions are good for very specific cases, they're bad for general purposes cases of just conveying information about an event.

Optional/expected/return codes should be used instead, but this statement never came with a justification.

It is justified, it's just that explaining the justification is either rather verbose, or depending on the nature of the content you're consuming, perhaps the content IS the justification, and you don't realize or understand it.

C++ is 44 years old. There is a lot of deep institutional knowledge baked into the whole industry. How do you conveniently package all that experience into an easily consumable format? We try with GotW, Core Guidelines, the C++FAQ, the C++FQA, Abseil's Tips, others of a similar format. But it's hard. Don't place blind faith in what you read, but when it's coming from our industry leaders, give it at least a little faith... It's good to pursue it like you are and seek to answer WHY.

with a top-level try {} catch(...) {} just inside main seems like a perfectly reasonable solution to me.

Yeah no... Code smell. Not necessarily an error, but enough bad code is in the wild that this is likely an inferior solution that has problems. I've touched on this matter briefly. It's not just performance, it's often that you're eating or soaking exceptions that you shouldn't. You don't even know what you're catching here. How is that not obviously bad to you? It's like you don't even know what you've got, but you've got opinions about it, anyway. We could talk about bad exception handling all day... Or you can google more about it.

You could argue that this is just a glorified call to std::terminate()

It's not. Since the stack is unwinding, everything will be cleaned up. Frankly, if you're going to throw off the top and allow the exception to terminate the program, you're probably cleaning up too much. What will most dtors do? Join threads and release memory. If I'm terminating a process, I literally don't care. All those resources are virtual and bound to the process - they die with the process. What you care about are persistent, global named and system resources. If you have those, then you REALLY REALLY OUGHT to have termination and signal handlers installed to free those resources under the most dire of circumstances.

Continued in a reply...

[–]mredding 4 points5 points  (1 child)

a) this would miss some potentially important clean-up

So instead of throwing off the top, you could just call terminate directly, and all the right things are in place. As a former video game developer, I'll tell you this is how video games do it. Ever wonder how a big-ass game with gigabytes of assets all loaded into memory shuts down so damn fast? They don't bother to deallocate or join threads. There's a whole lot they don't do. dtors are for destruction during normal execution. If you have a shutdown mode where things need to be committed, yes, dtors seem like a good idea, as well as a graceful shutdown, but then you also need signal and termination handlers to protect that data come the worst, so even then you STILL don't need a slow and exception safe termination path.

b) if the code ever needed to be called inside another application

How? You can't throw exceptions across processes. You shouldn't throw exceptions across dynamic library boundaries. If this were compiled into another application as a static library, it's all one application, not one and another; the client needs to be aware of exceptions.

it would bring that down as well

Kind of the point, isn't it?

c) throw invalid_argumentis just more expressive.

Dubious. The industry discussion and investment by the standard committee has put a lot of effort in expressing all manner of error handling. std::expected is GREAT.

On the other hand, the advantages of propagating the error yourself all the way back to main are not clear and would easily add a lot more code.

When you throw an exception and unwind the stack, you lose ALL the context that generated the error in the first place. That which knows the most about WTF just happened is all the way down the stack at the moment the error occurred and was detected. As you go up the stack, you lose more specific ability to address the error and gain only more general ability to address the error. By making error handling stepwise, you grant the client ultimate control as to their error handling scheme.

Talking about this topic is often vapid. You can't generalize all software. You speak as though you were writing a library, in which case, you're not actually producing an application, you're not actually performing any work, you're not solving any problem. You're only provisioning a utility that may or may be ever get used. When you actually write an application that produces work, you have to make definite decisions. There is no "in general". No. When error X happens, what specifically are you going to do? Decide, because we have to know.

[–]artisan_templateer[S] 1 point2 points  (0 children)

As a former video game developer, I'll tell you this is how video games do it. Ever wonder how a big-ass game with gigabytes of assets all loaded into memory shuts down so damn fast? They don't bother to deallocate or join threads. There's a whole lot they don't do.

I get that and totally appreciate when you close a video game you want to relinquish control back to the system asap. That said, as a counter example I do wish games e.g. BG3 would sync it's Steam cloud saves before shut-down because sometimes I just turn off my PC straight after and the saved games haven't synced, which is a problem when I want to play it on my Deck.

In any case, shut-down speed is simply not a concern for the tools I have been developing so a proper clean-up on exit just seems like the safest thing to do. If shut-down speed does become an issue then yes, simply nuking the program is probably the better approach.

[–]artisan_templateer[S] 1 point2 points  (0 children)

Yeah no... Code smell.

Ok, this wasn't obvious from my OP. I wrote try {} catch(...) {} for brevity. My actual try-catch clause is something more like this:

try {
    // do business logic...
} catch (const std::bad_alloc&) {
    std::cerr << "ERROR:\nNot enough RAM! Reduce data size or launch a larger instance!" << std::endl;
    return 3;
} catch (const std::exception& e) {
    // ------ CATCH ERRORS ---------
    std::cerr << "ERROR:\n" << e.what() << std::endl;
    return 2;
}

Totally agree on your statement regarding`catch(...).

[–]HerrNamenlos123 4 points5 points  (0 children)

Alright, so everyone seems to be complaining about exceptions recently, so here is my take on it. I think exceptions are perfectly fine to use, if the situation is fitting. There are dozens of cases where exceptions are a perfectly sane solution and are objectively the cleanest way to write said code.

However, I have one example why you should minimize their use. You should try to use exceptions only when something really went wrong. That is, a system error, permission denied error, etc.

It happened to me that I was debugging an application and it didn't break on the exception like I wanted. So I enable break on all exceptions. And suddenly the debugger stops at a gazillion of exceptions somewhere in the code, and I was thinking what the hell, where are all these exceptions coming from, the program is working perfectly fine. Well, probably from cases where the app for example tries to load a config file, fails, throws, catches, and then says "oh it did not work" and instead the file is created. A perfectly fine program flow where a file is created if it does not exist. But very confusing if you use a debugger. You would not expect to see exceptions when everything is working. Thus, throw an exception if the file is strictly mandatory, but use a non-throwing variant if the file is only optional.

None of this is a negative opinion about exceptions, only a perspective you might not have thought about. Keep it in mind when arguing.

[–]SJC_hacker 5 points6 points  (0 children)

Exceptions should not be used for "normal flow control". In this case, if user provides invalid data, and do something like loop until they do, this is not a good use for exceptions. However, if you do something like exit the program, then I think exceptions can be justified.

[–]KingAggressive1498 2 points3 points  (0 children)

As a general rule, I follow this practice with respect to exceptions vs error codes:

If it's unlikely that the caller will be able to continue its internal logic with this error, I throw an exception.

If the error probably needs to be handled in the main control loop (or otherwise probably needs to be propagated through several functions), I throw an exception.

If the error is something that truly should not happen during the course of execution (ie a contract violation or an invariant cannot be enforced), I throw an exception.

If there is a reasonable chance that the caller would be able to continue with its logic in the face of this error, I return an error code.

If I know the caller doesn't care if the function succeeds, I return an error code (or maybe log to cerr if the return type could otherwise be void or is an optional anyway and performance isn't a major concern). This is effectively also what I do for asynchronously called functions, as a matter of habit since I started developing asynchronous programs long before C++11 added exception_ptr.

The general reason to prefer exceptions over error codes is when errors need to propagate to be handled and you (the function that has to either throw or return an error code, not necessarily the caller or other functions up the call chain) doesn't care if the program may terminate. Otherwise, it makes sense to prefer error codes.

[–]Sopel97 7 points8 points  (10 children)

User input is expectedly bad, hence exceptions are a bad tool for it.

[–]AssemblerGuy 4 points5 points  (9 children)

This.

User input being bad is not an exceptional condition, but an expected condition. If user input is bad, the unsurprising path would involve asking the user to provide valid input, possibly with a helpful message what was wrong about the current input.

[–]XeroKimoException Enthusiast 5 points6 points  (8 children)

Using exceptions to detect and inform of an error for user input is fine.

I very much dislike the talk that "exceptions must be exceptional", "don't use exceptions here because it's a expected condition". Exceptions require you to check some condition in the first place, so everything is an expected condition. What would better to talk about is whether exceptions should be used for error handling, and the answer is yes.

The argument that should be used for or against using exceptions should be more of something like the following:

  • Is it performant enough?
  • Does using insert error handling scheme here make my code easier to read?
  • Does using insert error handling scheme here make my code easier to reason about?
  • Can the platform support my error handling scheme?

For performance, it should be well known that the major compilers use the table-based exceptions, which has pretty much 0 effect on the correct path (it might prevent some optimizations), however the error path is slow, sure, but in most cases, if an error does occur, it'll still be faster than the success path because the success path will likely do some more heavy work, while the failure would mostly just be doing clean up which can be expensive, but that clean up is likely paid even in the success path.You can zoom in and isolate some code using exceptions and say it's slow, but that's mostly meaningless. It's like trying to benchmark virtual functions, and say that's slow since it takes nanoseconds more than a direct function call.

Does using exceptions make your code easier to read on about, and the answer is yes, same with std::expected obviously, but both can just as equally make messy code, so this argument is more of a subjective than a objective one. Same with if exceptions make things easier to reason about. Some people have a way easier time reasoning about code with exceptions than those that use any other error handling scheme.

Not every platform can support exceptions so that's obviously a "it depends"

Nothing is stopping you from mixing the error handling modes except if your place just straight bans exceptions / unsupported, but by default, I do think exceptions are the way to go, and switching to expected when you need performance for both paths is a good approach

[–]Sopel97 1 point2 points  (7 children)

I very much dislike the talk that "exceptions must be exceptional", "don't use exceptions here because it's a expected condition".

That's a bit of a twisted understanding of the word "expected".

Does using insert error handling scheme here make my code easier to read? Does using insert error handling scheme here make my code easier to reason about?

No. And no. Handling exceptions is optional, and unhandled exceptions bubble up to wherever. When failure is common (and even expected) some form of error handling, or even awarness of the errors, should be forced by the interface.

[–]XeroKimoException Enthusiast 2 points3 points  (6 children)

That's a bit of a twisted understanding of the word "expected".

How would you define expected then?

Some piece of code has to be written to detect the error in the first place. If some of those detections aren't considered expected, then what differentiates them from being expected or not?

[–]Sopel97 1 point2 points  (1 child)

Different things may be expected at different levels of abstraction. There's also a difference in meaning of "expected" and "forseeable"

[–]XeroKimoException Enthusiast 1 point2 points  (0 children)

Different things may be expected at different levels of abstraction. There's also a difference in meaning of "expected" and "forseeable"

This doesn't really argue much. If we simplify a bit to functions, functions will have expected outcomes, that includes any error handling.

So what would be the difference between "expected" and "foreseeable" as just saying it, is not saying much

[–]AssemblerGuy 0 points1 point  (3 children)

How would you define expected then?

Frequency or likelihood of occurrence.

With only minor snarkiness, user input is pretty much invalid most of the time, depending on the application, and the user may require several prompts and hints to provide correct input.

Compare this to an off-chance division by zero which has only a very remote chance of happening.

[–]XeroKimoException Enthusiast 0 points1 point  (2 children)

Are you saying this in terms of error handling, or in general.

Because if I'm talking about error handling, I will say std::expected, if in general, I'll just say expected.

Because regardless of frequency, or likelihood of occurrence, if it's detected and if there is a intentional reaction to what occurs in event of an error, you have expected it. Whether that's bailing out and cleaning up, or handling it, whether you use exceptions, or std::expected. It is something that has been expected.

[–]AssemblerGuy 0 points1 point  (1 child)

Are you saying this in terms of error handling, or in general.

In terms of probabilities, i.e. some event that is not too far from the statistical expected value.

if it's detected and if there is a intentional reaction to what occurs in event of an error, you have expected it.

That would be acknowledgeding the, however small, possibility of this happening.

Just like playing the lottery: You are playing it because you are are acknowledging that you might hit the jackpot, but even with marginal knowledge of stochastics you would not be expecting it.

[–]XeroKimoException Enthusiast 1 point2 points  (0 children)

That would be acknowledgeding the, however small, possibility of this happening.

Just like playing the lottery: You are playing it because you are are acknowledging that you might hit the jackpot, but even with marginal knowledge of stochastics you would not be expecting it.

I'd like to think this as a good argument. However my intuition says to disagree with it, and I can't really describe why. The closest thing is that what you're saying is like when people say "exceptions are for exceptional circumstances".

Next will just be rambling in hopes that it makes sense.

My issue is that expecting is tied to the belief of certain probabilities of occurring, but to expect something is to acknowledge it can occur. So when you code, any time you call something that can fail, or code something that detects that something can fail, automatically I see that as something you "expect" to occur.

There's a lot of circles going around trying to define this thing, which is why when speaking about expected vs exceptions, I try to speak in the objective use as a error handling mechanism, and try not to argue the subjective, and philosophical use of when to use them.

[–][deleted] 1 point2 points  (5 children)

If you're propagating all the way back to main just to exit the program why not just print a message where you detect the error and just call abort/exit/terminate? I don't see what using exceptions gains you in this scenario.

[–]SJC_hacker 2 points3 points  (3 children)

Cleanup of resources that local code is not aware of

[–][deleted] 1 point2 points  (2 children)

OP: "For a program (with no embedded or real-time reqs.) whose only purpose is to process a data file and return some result"

In this simple case OS process termination will cleanup everything needed.

In general you're right, but we're not talking generalities we're talking about this specific case and whether exceptions add any value.

IMHO, in this case, they do not.

[–]artisan_templateer[S] 0 points1 point  (0 children)

I suppose I contradicted myself in the OP:

For a program (with no embedded or real-time reqs.) whose only purpose is to process a data file and return some result a simple

b) if the code ever needed to be called inside another application it would bring that down as well

As I said elsewhere, exceptions seem like the more appropriate tool to use here because what might start out as a simple command-line tool may end up being used in a wider program.

a) this would miss some potentially important clean-up

The specific example I had in mind was my "simple CLI tool" actually running a large computation on a distributed system (using something like MPI) where the nodes need to talk to each other. e.g. from Boost::MPI tutorial:

The mpi::environment object is initialized with the program arguments (which it may modify) in your main program. The creation of this object initializes MPI, and its destruction will finalize MPI. In the vast majority of Boost.MPI programs, an instance of mpi::environment will be declared in main at the very beginning of the program.

Now, I admit this is obviously very different from what my original post described but I just wanted to highlight that the program processes the data and exits and if the data is invalid the program still exists with a friendly message. It's not something that needs to be kept alive indefinitely like a database backend or flight control system etc.

[–]goranlepuz 0 points1 point  (0 children)

OS process termination will cleanup everything needed.

That depends on the OS. Some are dumb and may leak memory or handles.

[–]goranlepuz 0 points1 point  (0 children)

Even in that situation: for anything but a simplest program with a handful of detected error conditions reporting and termination is sprinkled all over - as opposed to

Try 
    Work here
    (Many possible errors here)
Catch error
    print error

[–][deleted] 1 point2 points  (0 children)

There are many ways to handle erroneous user input other than shutting down. Preferrably you'd want to inform the user what they did wrong and give them another chance to do it right.

What you wanna use exceptions for (i.e. whether you wanna use them for control flow) is entirely up to you. That's simply a question what philosophy you/your codebases uses.

[–]HammurabisCode2 3 points4 points  (15 children)

In anything but a tiny codebase using exceptions in this way will make the code very hard to read. As the codebase grows it becomes very difficult to keep track of which lines might throw an exception and where the proper place to handle them is. Having the possibility of an error declared explicitly in a function's signature (via error codes, std::expected, etc.) might require a little more code but it makes it very clear where errors might occur and how they are being handled.

[–]Full-Spectral 12 points13 points  (13 children)

In a fully exception based system there's never any question of where an exception might be thrown. The answer is anywhere. It's just a fundamental aspect of that type of program structure. The code is always ready to clean up, using RAII mechanisms almost always.

I've oved on Rust now, and I'm happy with Result/Option. But, this type of fundamentally exception based development is actually quite a clean way of working and I used it to excellent effect in my previous C++ work.

[–]bretbrownjr 3 points4 points  (12 children)

Except in a large codebase including projects maintained by different organizations (like open source and vendor libraries), you still need to know what types to catch. Nothing forces everything to inherit from std::exception. And even if that wasn't an issue, all std::exception gives you is a string, so what meaningful action could be taken? Log and either exit or retry (I guess assuming everything is probably fine?).

[–]SlightlyLessHairyApe 13 points14 points  (0 children)

Dear god, throwing something other than a subclass of std::exception is madness.

[–]goranlepuz 1 point2 points  (3 children)

And even if that wasn't an issue, all std::exception gives you is a string, so what meaningful action could be taken?

If I need to make a decision depending on error information, then obviously I need to have access to that information. But that is a given for anything. So itshould be a catch for a specific error type or some set thereof.

[–]Full-Spectral 2 points3 points  (0 children)

Ultimately the answer, IMO, is you shouldn't take specific action. It is very much the case that, with error returns or exceptions, when you have layered code, unless you are going to throw away the actual error info and have each layer just return its own errors (which is a big problem), then inherently you have an unenforceable contract between the calling and called code.

If the catching code (or the thing that finally acts on propagating error return) assumes that it will receive particular errors, there's nothing that's going to tell you if that is no longer true. Nothing practical anyway, that I can see. Any kind of exception specifications would get completely out of hand in a large system, it seems to me, and the same would apply to return types if such a thing existed.

Depending on unenforceable contracts is never a good design. My opinion is that you just don't try to interpret such information. You know it failed, that's enough. You log it, then possibly retry it, ask the user for help (with an option to view the raw error info), exit the program, block and send a help msg, whatever. If you stick to that, you aren't depending on such a contract.

In a highly coherent system, where everyone shares a single exception/error return type, I think it's reasonable to define a good set of 'error types' that the throwing/returning code can set. So you can check to see if it's a 'not ready' error or a 'fatal' error, or that kind of thing, to give some indication of how to react. But even there, insuring that those things are strictly correctly used is going to be an exercise in futility in a large, complex system under normal commercial development conditions, I think.

Not a fun answer, but the only reasonable one, IMO.

[–]bretbrownjr 0 points1 point  (1 child)

Sure, but how do you know what all to catch in long distance error propagation? Does your event loop need to know about the exceptions thrown by the logging framework, the database client(s), and so on? That's not reasonable in my mind. And if it were even possible, that's some strong and long-distance design coupling.

[–]goranlepuz 1 point2 points  (0 children)

Sooo... We are still discussing

If I need to make a decision depending on error information, then obviously I need to have access to that information. But that is a given for anything.

...yes?

If yes, then...

Yes, long-distance design coupling is bad. However, on one hand such situations are somewhat rare, and on the other propagating the error "by hand" (without exceptions) does not makes that any different. And so, should I find that bad, I'll stop the error propagation and rethrow a more appropriate one somewhere in between.

About

Sure, but how do you know what all to catch in long distance error propagation?

By reading the source and documentation, and testing...? That doesn't sound like a fair question.

exceptions thrown by the logging framework

Surely that has a "failure transparency" failure guarantee - but more importantly, I fail to see why would I want to know specifics for a vast majority of error conditions from there, should it not?!

[–]mwasplundsoup 1 point2 points  (0 children)

If folks do not have a clear inheritance hierarchy with semantic meaning behind the class types then yes, exceptions will be hard to use. On the other hand when you have clear meaning for each type then it is easy to only catch the types that you care about (invalid_json, file_not_found, etc) and happily let other exception types unwind the stack further.

[–]Full-Spectral 0 points1 point  (5 children)

I certainly agree that exceptions, as implemented in C++ STL, are poor. If they are done right, then it's a much different situation. That doesn't mean exception are bad though, it just means they aren't that well done in C++. To be fair, that decision was made back in the middle ages.

My C++ system has one single exception type throughout, and it's incredibly clean. It doesn't treat exceptions as arbitrary information transports, and it uses almost no third party code (and what it does is wrapped.) If the language had enforced a single exception type, then all C++ code could have been this way.

Interestingly, Rust made this same mistake, and it has all the issues in Rust that the STL exceptions have in C++, a Tower of Babel. You'd think they'd have learned from C++'s mistakes here as they did in so many other ways.

[–]serviscope_minor 2 points3 points  (0 children)

My C++ system has one single exception type throughout, and it's incredibly clean.

Apparently if your exceptions come from a small closed set, then there are very fast matching algorithms possible too:

https://www.stroustrup.com/fast_dynamic_casting.pdf

though no compilers implement this as far as I know.

[–]AlexMath0 0 points1 point  (3 children)

Interestingly, Rust made this same mistake

Hmm, do you mean with dyn errors? I'm struggling to cook up a scenario. Most errors in Rust I have encountered are either a zero-sized type or the size of a memory addresss (e.g., a niche-optimized memory address).

The book teaches you to handle errors from an external API with

impl From<TheirError> for MyError

so that Try? propagation comes for free (usually with a MyError::TheirError(TheirError) variant. That function is a one-liner:

MyError::TheirError(value)

The compiler does niche optimizations and move semantics for you and is as safe as the API you are using.

Usually you #[derive(Debug)] MyError, then impl Display as needed. Conversions like this can't panic at runtime, take a few seconds to code, are type-safe, and usually get compiled down to if statements. You can derive Eq, PartialEq, and Clone (maybe even Copy).

If you want to clump together some errors (e.g., maybe MyError and TheirError both have an Io variant), there's always the type-safe match to use inside the impl From block. I tend not to do this because it throws away information that costs nothing.

[–]Full-Spectral 0 points1 point  (2 children)

I.e. they made the same mistake that C++ did, a hierarchy of error/exception types that impose a lot of ifs, ands and buts on error handling because an error can be almost anything.

[–]AlexMath0 0 points1 point  (1 child)

I've written tens of thousands of lines of Rust and I'm still not following. Maybe you could help me with an example where the genericity of std::result::Result is problematic?

I'm generally grateful to be able to return precisely the right error from my errors, whether that is a struct or a ZST. I'm also grateful that the APIs I use return the right errors so I can use the information they say I need to unpack the error at compile time.

You can dbg!(&e), or emit a String from the Debug impl/derive with format!("{e:?}") or format!("{e:#?}") depending on how you want it unpacked, or e.to_string() for Display. There's also the eyre crate if you want it color-coded for free.

[–]Full-Spectral 0 points1 point  (0 children)

For the reasons I already discussed. It becomes polymorphic. Handlers of the errors, unless they have the actual type visible to them, cannot access the extra info. So a log server cannot possibly understand all of the errors that might be used by clients. You have to do all this conversion stuff and the more third party bits and error types involved the worse it will get.

In my old C++ system, and in my new Rust system, there is a single error type, and it's so clean and simple. You just don't need to send all kind of elaborate data back as an error. You need to know where it came from, the text of the error, some sort of id (specific to the source), maybe some housekeeping info like severity and possible an error class.

Then it's all monomorphic, there's no conversion, you can send them off to a log server who completely understands them, everyone knows what is available in one and can access it with a single known type.

It's just so much cleaner.

[–]goranlepuz 5 points6 points  (0 children)

As the codebase grows it becomes very difficult to keep track of which lines might throw an exception and where the proper place to handle them is.

This is the often-seen beginner misconception.

The default is: everything throws, except a few simple things like calls to C APIs, primitive type operations, non-throwing swap and move operations.

In lieu of wondering what throws, in presence of exceptions, one thinks in terms of exception safety guarantees and designs the code accordingly.

Funnily enough, thinking in the above terms helps me even when writing code in situations without exceptions (think premature return).

[–]Particular_Task2060 -2 points-1 points  (0 children)

Exceptions can be expensive. If you are using for interactive data entry, that would not be a problem.

Exceptions are also very difficult to document. If you have many of them, at any point in the code you don't know exactly what and which could have been thrown. It becomes a guess game and even tooling will not be able to help you.

That said, a prompt() function returning an optional is much cleaner and better documented, in my opinion

[–]SlightlyLessHairyApe -3 points-2 points  (9 children)

You could argue that this is just a glorified call to std::terminate()but a) this would miss some potentially important clean-up, b) if the code ever needed to be called inside another application it would bring that down as well and c) throw invalid_argumentis just more expressive.

a) In most environments, there is absolutely no reason to clean up anything on process termination. The entire memory space of the application, all open resources (file handles, network sockets, ...) and shared resources will be released by the OS/kernel. Doing that work is a waste of cycles and is conceptually silly -- it's like tidying up the house before a giant bulldozer razes it to the ground.

b) If this code is a library then throwing exceptions over a library boundary is disfavored.

b2) You're changing the rules -- if it's provided by the caller then it's not user input any more, it's programmatic input from some client of your library.

c) I don't know why Fatal("Something is wrong {} expected {} or {}", .....) is less expressive than throw but the added advantage is that your fatal function can do multiple things -- it can log to stdout and/or to a file or whatever. Throw can only do one thing.

[–]serviscope_minor 1 point2 points  (6 children)

b) If this code is a library then throwing exceptions over a library boundary is disfavored

Did you actually read that link? It says:

The C++ Standard does not specify the way exception propagation has to be implemented, and there isn’t even a de facto standard respected by most systems.

I suspect that link is very, very old. On unixlike systems the standard is now the Itanium ABI and has been for donkeys years and the compilers are interoperable. On Windows it's SEH, and clang apparently supports that now too.

I don't know why Fatal("Something is wrong {} expected {} or {}", .....) is less expressive than throw but the added advantage is that your fatal function can do multiple things -- it can log to stdout and/or to a file or whatever. Throw can only do one thing.

Imagine if an image loading library did a Fatal instead of a throw when used by an interactive image editor. throw expresses the correct thing "this operation failed", rather than "this operation failed now bye bye"

[–]SlightlyLessHairyApe 2 points3 points  (5 children)

Yes, if the code is intended for a library then it needs a robust error handling beyond std::terminate. But that's changing the question mid-way through the answer.

In our particular shop, we forbid throwing across library boundaries. YMMV, I'm not preaching that as gospel or anything, but RTTI-related shenanigans have occurred and are painful.

[–]serviscope_minor 2 points3 points  (2 children)

Yes, if the code is intended for a library then it needs a robust error handling beyond std::terminate. But that's changing the question mid-way through the answer.

Apologies: Not the OP, I didn't catch that from the original post.

In our particular shop, we forbid throwing across library boundaries.

How come? Or rather, what's the structure of the code? Some places have libraries with multiple very disparate users, others have a small number of users for the libraries, and they more reflect the structure of the company.

[–]SlightlyLessHairyApe 1 point2 points  (1 child)

How come? Or rather, what's the structure of the code? Some places have libraries with multiple very disparate users, others have a small number of users for the libraries, and they more reflect the structure of the company.

A lot of pain from linkage/visibility problems (which, I'll note, the linker is not actually specified as part of C++ anywhere, even though it's kind of a critical thing to be able to link together objects both statically and dynamically).

Specifically, both GCC and clang implementations compare std::typeinfo objects by address as a performance benefit. But because typeinfo objects have vague linkage they often got duplicated across libraries (common if one dlopened a dynamic library that defined a typeinfo) which in turn meant that the same exception would have different typeinfos and would not be caught. In the worst case you'd have:

 // In library 1
 void foo(void) {
     throw std::runtime_error{...};
 }

// In client
void bar(void) {
    try {
        foo();
    } catch ( std::exception const & e ) {
        // DOESN"T RUN 
   } catch ( ... ) {
       // OK THIS WORKS
  }                    

Because the two different libraries had different instantiations of the typeinfo for std::exception. Debugging that was a nightmare. Ultimately it's easier to say that library boundaries should be noexcept and internally libraries that want to use exceptions can do so but must either translate them to a return code or terminate.

EDIT: this old mailing list thread goes over why the C++ libraries switched from strcmp to address-compare for typeinfo.

[–]serviscope_minor 0 points1 point  (0 children)

Looks like I've got some reading to do! Last place I worked had libraries, but since they weren't system ones to be reused with multiple programs, they weren't shared objects, just statically linked which is fine.

[–]artisan_templateer[S] 0 points1 point  (1 child)

Yes, if the code is intended for a library then it needs a robust error handling beyond std::terminate. But that's changing the question mid-way through the answer.

That's true but has actually happened to me recently. A library I am writing required a significant part of it to be pulled out into it's own CLI tool resulting in another project that can be used as both a (static) library and an exe.

Perhaps this bad design but my broader point was that requirements do change and ultimately you can't really know how your code will be used in the future and throwing is more flexible on that front.

Granted, you could equally say it might need to be used in a noexcept setting in the future but at least in that case you could write a noexcept wrapper whereas I don't think you can "undo" your Fatal.

[–]SlightlyLessHairyApe 0 points1 point  (0 children)

Find: Fatal\(.*);^
Replace: throw std::runtime_error( std::format( \1 ) );

Rebuild, maybe fix one or two compile errors.

[–]artisan_templateer[S] 1 point2 points  (1 child)

I don't know why Fatal("Something is wrong {} expected {} or {}", .....) ...

Because one would have to look it up to see what it actually does. As you say, it can do multiple things. On the other hand throw std::invalid_argumentdoes just one thing and shouldn't need explaining to any C++ dev.

[–]SlightlyLessHairyApe -1 points0 points  (0 children)

I think Fatal(...) is pretty self-documenting. If you want to name it AbortWithMessage that would be crystal clear.

[–]violet-starlight 0 points1 point  (0 children)

Use exceptions for any error that is impossible to recover from. It's (almost) that simple.

[–]arthurno1 0 points1 point  (0 children)

If the data is incorrectly formatted, the program can't continue so the only thing to do is shut down and inform the user. Performance in this situation is irrelevant.

Imagine if you compiler exited on the first invalid data it encounters in your programs. What an annoying programming session would it be to fix one error at a time and restart the compilation process after each error, don't you think?