Live data from Hacker News

Problems with C++ exceptions

marler8997.github.io

101–110 of 112 posts

Re: Problems with C++ exceptions

#101
post #97

Earlier quoted context omitted.

This is error-prone boilerplate that obscures the code, obscures the logs and is a known antipattern (log and throw) - which you're implementing manually, by hand, in the hope you never make a mistake. You shouldn't do manually what you can automate. Boilerplate can make you feel productive and can give you warm fuzzies inside when you see lots of patterns that look familiar, but seeing the same patterns over and ove…

No. If you want to have the same information without that approach, you need to implement nested levels of error information, ten layers deep, each layer having a diagnostic, a log level, and file, line information, and a reason. In other words, you are building your own custom stack trace object, with annotated diagnostics. In addition, you can't reason about this at the upper level anyway, or you are rebuilding you…

The information about what the code was doing at the time an exception is thrown is implicit in the stack trace; the control flow is evident from line numbers, and with half-decent tooling, you can get hyperlinks from stack traces to source code.

If there's extra information you need to know about what's going on, you probably want to log it in non-error cases too, because it'll contextualize an error before the error occurs. You can't log on unwind for decisions made in routines that were invoked and returned from, and are no longer on the stack, in the error case.

> Why is it error-prune. You receive an error of one type and need to convert it into an error of another type. You need to handle that or the compiler will bark.

This is a choice you've made, not something inherent to unchecked exceptions. My argument is that you should not generally change the type of the error, and let polymorphism cover the abstraction. It's error prone because it's code you need to write. All code you write is on the cost side of the ledger and liable to contain mistakes.

> How does it obscure the logs?

You get lots of error log messages for a single error.

> In addition, you would need to allocate on a failure path, which sounds like a nightmare.

I wanted to address this separately. Allocating on the failure path is only a real problem in OOM or severe memory corruption scenarios, in which case logging may not be available and the best course of action is probably to abort the program. In this case, you want the logs that led up to the abort, rather than trying to log positively under severe memory conditions.

Are you a Go user by any chance? I've stayed away from Go precisely because of its boilerplate-reliant error handling mechanism. Or if you're stuck with C++ (with or without exceptions), my commiserations.

Re: Problems with C++ exceptions

#102
post #96

Earlier quoted context omitted.

So you want to gradually reduce the fidelity of the error message as it makes its way up the stack. That means that the top level handler can at best log a vague message. That in turn means you must log along the way where you have more precise information about the failure, or you risk not having enough information to fix issues. And that in turn means you must have lots of redundant logging, since each point in the…

> That in turn means you must log along the way where you have more precise information about the failure Yes, that's the idea. You split the information into a diagnostic with stuff you deal with now and data that the upper layer will handle. The intersection between the data in these two things should be empty. > And that in turn means you must have lots of redundant logging, since each point in the stack doesn't k…

The problem with this is you don't have context on subgraphs of control flow which have already returned by the time of the error.

I think our disagreement is less about error handling and more about logging policy. I favour a logging policy which lets you understand what the code is doing even if it doesn't error out; this means that you don't need to log on unwind. You favour a logging policy specific for errors.

My position is that your position ends up with less useful context for diagnosing errors.

Re: Problems with C++ exceptions

#103

Earlier quoted context omitted.

> That in turn means you must log along the way where you have more precise information about the failure Yes, that's the idea. You split the information into a diagnostic with stuff you deal with now and data that the upper layer will handle. The intersection between the data in these two things should be empty. > And that in turn means you must have lots of redundant logging, since each point in the stack doesn't k…

The problem with this is you don't have context on subgraphs of control flow which have already returned by the time of the error. I think our disagreement is less about error handling and more about logging policy. I favour a logging policy which lets you understand what the code is doing even if it doesn't error out; this means that you don't need to log on unwind. You favour a logging policy specific for errors. M…

There is no reason you can use that scheme only for errors? In fact I don't.

I use "unwinding" for diagnostics even in the happy case.

Re: Problems with C++ exceptions

#104

Earlier quoted context omitted.

No. If you want to have the same information without that approach, you need to implement nested levels of error information, ten layers deep, each layer having a diagnostic, a log level, and file, line information, and a reason. In other words, you are building your own custom stack trace object, with annotated diagnostics. In addition, you can't reason about this at the upper level anyway, or you are rebuilding you…

The information about what the code was doing at the time an exception is thrown is implicit in the stack trace; the control flow is evident from line numbers, and with half-decent tooling, you can get hyperlinks from stack traces to source code. If there's extra information you need to know about what's going on, you probably want to log it in non-error cases too, because it'll contextualize an error before the erro…

> The information about what the code was doing at the time an exception is thrown is implicit in the stack trace; the control flow is evident from line numbers, and with half-decent tooling, you can get hyperlinks from stack traces to source code.

Yes, but when you want to generate a diagnostic from that stack trace, you basically need to branch on all the possible internal states of internal layers at the point you catch the exception. So either you are incapable of generating detailed diagnostics or you essentially model the whole behaviour in a single place. Also the point where the error messages are generated is now completely removed from the place where the error did occur. This sounds like a nightmare to maintain and also means that the possible error messages aren't there as documentation when reading the code.

> If there's extra information you need to know about what's going on, you probably want to log it in non-error cases too, because it'll contextualize an error before the error occurs. You can't log on unwind for decisions made in routines that were invoked and returned from, and are no longer on the stack, in the error case.

I never said, that you can only use this for error cases. In fact what is an error and what not, is not defined in a single layer. For example a failure to open a file will be an error in a lower layer, but for an upper layer, that just means that it should try a different backend. Or a parsing error is fatal for the parser, but it might mean that the file format is different and the next parser should be tried, or that the data is from the network and can simply be discarded. An empty field can be normal data for the parser, but for the upper layer it is a fatal error.

> This is a choice you've made, not something inherent to unchecked exceptions. My argument is that you should not generally change the type of the error, and let polymorphism cover the abstraction.

Then either the reported errors are completely unspecific and unactionable or you are leaking implementation details. When you want to handle every error specifically and not leak implementation details, you need to handle it locally. When you want to know that you handle all cases, unchecked exceptions are unsatisfying. In my opinion programs should know what is going on and not just say "my bad, something happened I can't continue". That does not lead to robust systems and is neither suitable for user transparency nor for automated systems.

In my opinion software should either work completely automated or report to the end user. Software that needs sysadmins and operators at runtime is bad. That doesn't mean that that never occurs, it will, but it should be treated as a software defect.

> You get lots of error log messages for a single error.

Yes, but this describes the issue at different layers of the abstraction and in my eyes the whole thing is a single error message. Neither the fact that resource X isn't available nor the fact that some connection was refused, is a complete error description in isolation. You need both for a (complete) error description.

> Allocating on the failure path is only a real problem in OOM

Yes, but first I don't like my program to act bad in that case, and second, it is also a nightmare for predictable ownership semantics. I generally let the caller allocate the error/status information.

> Are you a Go user by any chance?

I have never used Go, not even tried, but what I read about the error mechanism appealed to me, because it matches what I think is a good idea and do anyway.

> Or if you're stuck with C++ (with or without exceptions), my commiserations.

I don't feel that unhappy with that approach. I think this is a design and architectural decision rather than a language issue.

> with or without exceptions

Currently, definitely without, because it isn't even available when targeting a free-standing implementation (embedded), but I also don't prefer them, it makes for unpredictable control flow and makes it hard to reason about sound-, complete- and exhaustiveness.

You seem to have the impression, that you can just panic and throw a stacktrace. That might work fine for a program running in the terminal and targeting developers, but it is not acceptable for end users nor for libraries. I also know programs that just output a stacktrace and crash. That is stupid. I mean I understand what is going on, because I am a developer, but first I am not familiar with every codebase I use, and second the average end user is not able to act on any of that and will be angry for good reason when it's documents are gone, data is corrupted or even the workflow is interrupted. I also don't perceive a network error, a file (system) issue or OOM to that rare for it to be acceptable, to just ignore it. I should be part of normal program behaviour.

Re: Problems with C++ exceptions

#105
post #62

Earlier quoted context omitted.

Don't forget, failure modes pierce abstraction boundaries. An abstraction that fully specifies failure modes leaks its implementation. This is why I think checked exceptions are a dreadful idea; that, and the misguided idea that you should catch exceptions. Only code close to the exception, where it can see through the abstraction, and code far away from the exception, like a dispatch loop or request handler, where t…

This is a better way of expressing what I had been thinking about putting exception details behind an interface, except that in my mind encapsulating errors is just good design rather than implementation hiding, since the programmer might want to express a public error API, for example to tell the user whether a given fopen failed due to not finding the file or due to a filesystem fault.

e.what()

Just include the file name in the error message. And all of this is predicated on logging errors, which is not at all user-friendly, and not remotely acceptable in GUI applications.

Re: Problems with C++ exceptions

#106
post #62

Earlier quoted context omitted.

Don't forget, failure modes pierce abstraction boundaries. An abstraction that fully specifies failure modes leaks its implementation. This is why I think checked exceptions are a dreadful idea; that, and the misguided idea that you should catch exceptions. Only code close to the exception, where it can see through the abstraction, and code far away from the exception, like a dispatch loop or request handler, where t…

If your error codes leak the implementation details through the whole call stack you are doing it wrong. Each error code describes what fails in terms of it's function call semantics. A layer isn't supposed to just return this upwards, that wouldn't make sense, but to use it to choose it's own error return code, which is in the abstraction domain of it's function interface.

Presumes that an error code provides sufficient context to determine what the actual problem is. It does not. Which is why exceptions carry some form of text error message which may include (for example) the name of the file that could not be opened because permission was denied.

Awful stuff.

Re: Problems with C++ exceptions

#107
post #16
post #10

Earlier quoted context omitted.

Agreed. Author is trying to mix paradigms. Simplest approach if they want local handling and non-propagation of errors is to just have the file holder not check for open success, and check that manually after construction. Then you get guaranteed closure of file no matter how the function is exited. class File_handle { FILE *p; public: File_handle(const char *pp, const char *r) { p = fopen(pp, r); } ~File_handle() {…

this may lose the value of errno, right?

Kind of the opposite. You lose the fragility and imprecision of and non-extensibility of errno. Good riddance!

Re: Problems with C++ exceptions

#108
post #84

If C++ had a contract on what exceptions a function can throw with compile time check to enforce caller catches those exceptions, would it make it better? Guess Java does that, not much experience in Java here.

Java does it. It is a horrible horrible feature. C# used to do it. But they decided it's a horrible horrible feature, and removed it.

Re: Problems with C++ exceptions

#109

Honestly, I thought the diatribe would focus on needless complexity. The starting example is how I'd do it in C: ``` void f(const char* p) // unsafe, naive use { FILE \*f = fopen(p, "r"); // acquire // use f fclose(f); // release } ``` Wouldn't the simpler solution be ensuring your function doesn't exit before release? All that c++ destroyer stuff appears somewhat unnecessary and as the author points out, creates eve…

It's a solution that addresses a fundamental problem in C: that it's often baroquely complex to do that in C, and incredibly easy to make mistakes. It is possible, but it is very often not at all simple. Thanks, but no thanks.

Re: Problems with C++ exceptions

#110

Earlier quoted context omitted.

This is a better way of expressing what I had been thinking about putting exception details behind an interface, except that in my mind encapsulating errors is just good design rather than implementation hiding, since the programmer might want to express a public error API, for example to tell the user whether a given fopen failed due to not finding the file or due to a filesystem fault.

e.what() Just include the file name in the error message. And all of this is predicated on logging errors, which is not at all user-friendly, and not remotely acceptable in GUI applications.

I was talking about catching different classes, not about logging. Even if you’re just implementing a string description, there’s nothing stopping you from designing that string for use in a UI element, or implementing a structured message that can specify more details usable for defining UI elements, similarly to how many REST HTTP-driven web UIs work. I don’t think I’m quite following your criticism.
Post reply on HN