It seems all too often we (coders) are encouraged to think of errors as these exceptional things that happen rarely; deserving only of a few cursory preventative treatments to the code -- or worse, treated as something only to be fixed lazily, as bugs and crashes surface during testing. Indeed -- this philosophy is ingrained into a significant percentage (if not the majority) of the programming languages we use!
On the contrary, I believe we should expect to spend the majority of software engineering time writing and thinking through error-handling code, before even your first test[1].
I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
This is why I so greatly appreciate languages like Rust and Zig[2] which remove exceptions and unchecked null pointers from the language entirely (with few edge-case exceptions), and provide a powerful and robust type system that allows us to express and handle error conditions naturally, safely, and elegantly.
[1] To be clear, by no means am I downplaying the importance of test code, or even manual testing; rather, I'm arguing that purely "test driven development" is not sufficient to yield extremely robust software of significant sophistication.
[2] These aren't the only examples, but they're among the only that aim to be C++ (Rust) and C (Zig) replacements, that also made the "right" design choices (IMO) in removing both exceptions and unchecked null references.
This strategy tends to fail economically. The tech startups that succeed are usually ones that let their customers do things they would not otherwise be able to do. Usually doing something that nobody has done before is hard enough without considering the corner cases; if it follows a typical 90/10 rule, then doing 100% of the job will take 10x as long as the competitor who's only doing the easiest 90%, and your market will have been snapped up by them long before you can release a product. Customers would rather use a product that works 90% of time than do without a product entirely, at least if it delivers functionality they really want but can't get elsewhere (and if it doesn't, your company is dead anyway).
Once you've got a commanding lead in the marketplace you can go back and hire a bunch of engineers to finish the remaining 10% and make it actually work reliably. That's why solutions like testing & exceptions (in GCed languages) succeed in the market: they can be bolted on retroactively and incrementally make the product more reliably. It's also why solutions like proof-carrying code and ultra-strong (Haskellish) typing fail outside of markets like medical devices & avionics where the product really needs to work 100% at launch. They force you to think through all cases before the program works at all, when customers would be very happy giving you (or a competitor) money for something 80-90% done.
Someday the software market will be completely mature, and we'll know everything that software is good for and exactly what the product should look like and people wouldn't dream of founding new software startups. At that point, there'll be an incentive to go back and rewrite everything with 100% solid and secure methodologies, so that our software has the same reliability that airline travel has now. That point is probably several decades in the future, though, and once it happens programming will not be the potentially extremely lucrative profession it is now.
I'd agree that it's totally reasonable to 'hack together' a quick prototype with 'duct-tape and cardboard' solutions -- not just for startups, but even in full-scale engineering projects as the first pass, assuming you intend to throw it all away and rewrite once your proof-of-concept does its job.
The problem is that these hacky unstable unreliable solutions sometimes never get thrown out, and sometimes even end up more reliable (via the testing and incremental improvement methods you mention) than a complete rewrite would be -- not only because writing reliable software is hard and takes time (beware the sunken costs fallacy here!), but because sometimes even the bugs become relied upon by other libraries/applications (in which case you have painted yourself into a REALLY bad corner).
It's a balance, of course. You can't always have engineering perfection top-to-bottom (though I would argue that for platform code, it has to be pretty close, depending on how many people depend on your platform); if you shoot too high, you may never get anything done. But if you shoot too low, you may never be able to stop drowning from bugs, crashes, instability, and general customer unhappiness no matter how many problem-solver contractors you may try to hire to fix your dumpster fire of code.
So again: Yes, it's a balance. But I tend to think our industry needs movement in the "more reliability" direction, not vice versa.
This is simply not my experience with exceptions. Exceptions are frequently thrown and almost never need to be caught, and the result is easy to reason about.
My main use case for exceptions is in server code with transactional semantics. Exceptions are a signal to roll everything back. That means only things that need rolling back need to pay much attention to exceptions, which is usually the top level in jobs, and whatever the transaction idiom is in the common library. There is very little call to handle exceptions in any other case.
GC languages make safe rollback from exceptions much easier. C++ in particular with exceptions enabled has horrible composition effects with other features, like copy constructors and assignment operators, because exceptions can start cropping up unavoidably in operations where it's very inconvenient to safely maintain invariants during rollback.
Mutable state is your enemy. If you don't have a transaction abstraction for your state mutation, then your life will be much more interesting. The answer isn't to give up on exceptions, though, because the irreducible complexity isn't due to exceptions; it's due to maintaining invariants after an error state has been detected. That remains the case whether you're using exceptions, error codes, Result or Either monadic types, or whatever.
Not sure which type of "server" you meant when you said that, is that in the narrow sense of database server?
Behaviors similar to the above are not that infrequent, are expected from many other servers-in-wide-sense: media decoder would drop all decoding in progress and try to resynch to the next access unit, a communication front-end device would reset parts of itself and start re-acquiring channels (such exception-like reaction is even specified in some comm standards). Network processor would drop the packet and "fast-forward" to the next. Etc.
You could argue that this still looks like a server behavior loosely defined (and I agree), but a) this makes application field for exceptions large enough IMO, and especially b) how differently could one implement all that with other mechanisms (like return codes), and for what benefits?
> This is simply not my experience with exceptions. Exceptions are frequently thrown and almost never need to be caught, and the result is easy to reason about.
I write GUI apps and that is also how I use exceptions - and it works just fine. If you have an exception, the only rational thing to do most of the time is to let it bubble up to the top of the event loop show a warning to the end user or cleanly quit the program while making a backup of the work somewhere else.
And this is part of why I never ever ever "write one to throw away". It's very rare that it actually gets thrown away and redone "properly".
Also I just don't want to waste my time writing something that's for sure going to be discarded. There's a middle ground between "write something held together with duct tape" and "write the most perfect-est software anyone has ever written". My goal is always that the first thing I write should be structured well enough that it can evolve and improve over time as issues are found and fixed.
Sometimes that middle ground is hard to find and I screw up, of course, but I just think writing something to throw away is a waste of time and ignores the realities of how software development actually happens in the real world.
This. Once the spaghetti code glued together to somehow work is deployed and people start using it, it's production system and next sprint will be full of new feature stories, nobody will green light a complete rewrite or redesign.
And that’s how you get a culture where severe private data breaches and crashy code are the status quo :/
We can do better. Why don’t we? I guess the economical argument explains most of it. I think if more governments would start fining SEVERELY for data breaches (with no excuses tolerated), we’d see a lot more people suddenly start caring about code quality :)
>We can do better. Why don’t we? I guess the economical argument explains most of it. I think if more governments would start fining SEVERELY for data breaches (with no excuses tolerated), we’d see a lot more people suddenly start caring about code quality :)
Governments care about the "economical argument" even more so. They don't want to scare away tech companies.
Besides, today's governments don't protect privacy, rather the opposite.
We got a green light for a complete rewrite, but only because of licensing issues with the original code. I'm just hoping we don't fall for the second-system syndrome.
There are exceptions of course. I have also been involved in some complete rewrites and green field projects to replace existing solutions but it's very rare. Happens much more often in government sphere compared to private sector.
Which is the mistake, the throwaway should test one sub system or the boundary between two sub systems and nothing more. To get tautological again, once you have a working system you have a system.
With that "works 90% of time" idea, please don't ever involve yourself in software for anything serious: air traffic control, self-driving cars, autopilots, nuclear reactor control, insulin pumps, defibrillators, pacemakers, spacecraft attitude control, automated train control, the network stack of a popular OS, a mainstream web browser, a Bitcoin client, the trading software of a major exchange, ICANN's database, certificate signing, ICBM early warning system, cancer irradiation equipment, power steering, anti-lock brakes, oil/gas pipeline pressure control, online tax software...
I actually do have some experience in that area - one of my early internships was in a consultancy that specialized in avionics, health care, finance, and other areas that required ultra-high-assurance software.
It is a very different beast. Their avionics division was a heavy user of Ada, which you will basically never find a webapp written in. There are traceability matrixes for everything - for each line of code, you need to be able to show precisely which requirement requires that code, and every requirement it impacts. Instead of testing taking maybe 5% of your time (as with startup prototype code) or 50% of your time (as with production webservices), it took maybe 80% of the total schedule.
Not working in those fields either, but i don’t understand how people can be comfortable writing life-or-death code in C either. Anything that doesn’t involve a heavy dose of formal proof or automatic validation of properties of your code seems irresponsible as well.
A market subject to regulation would just move what’s considered the easiest 90%. Maybe in a small startup, one would write a fancy nonlinear or deep ML model in TensorFlow while for a regulated/compliance-oriented codebase, you’d stick to linear algebra for the ML model to guarantee convergence.
Agree with this except for the last bit about “mature” software market. Software is just the execution of logical processes. There’s no reason to think we’ll run of out need for new logical processes to be implemented.
Errors and exceptions are really fairly different things, and I personally appreciate languages like Erlang that have both.
"Errors" are an instance of a (perhaps-implicit) returned sum-type of a given function. Calling a function that returns a sum-type, and then not having a branch for each instantiation of that sum-type, is almost always a logic error in your code, no matter what the sum-type is.
A pretty good generic solution to "error-handling" is the Either monad, as seen in its literal form in Haskell, in Javascript as a convention for async callbacks, and in Erlang as the conventional sum-type of {ok, Val} | {error, Desc}. The Either monad is nice because you can't really "lower" monadic code by ignoring the monad and just "getting at" the result; instead, you have to "lift" your code into the realm of the monad, which requires you to specify how you'll handle each of its potential cases. (Even if your specification is simply "I expect this; if it isn't this, crash"; or "I expect this; early-return anything else to my own caller.")
"Exceptions", on the other hand, are things that by default no component of your system knows how to handle, and which usually—other than some infrastructure-level components like HTTP error handlers and "telemetry" service libraries—code just allows to bubble up until it hits the runtime and becomes a process abort. These include "runtime errors"—for example, failures of memory allocation; and "programmer logic errors"—for example, dividing by zero.
In either of these cases, the "correct" thing to do is "nothing at all", because the system itself has no idea how to handle these at runtime. The only time when these problems could have been handled was at development time, by writing more or different logic such that these exceptional circumstances would never arise in the first place. Now that you're in such a situation, though, you pretty much just want to note down what happened (so the log can be sent to a developer, who can use it to understand what the novel exceptional situation was, and then plan a change that will prevent the exceptional situation in the future) and then die quickly, before your now-invalid runtime state can do harm to things like durable stored state.
Or, to put that another way:
• an error says "I failed to do that."
• an exception says "I have noticed, while trying to do that, that no valid system-state can result, even one of failure."
I don't think this alleged distinction between exceptions and errors is as clear-cut as you imply. I think this distinction is purely a convenience; a line we draw to make our lives easier because we don't really want to accept that extremely rare error cases do exist and should be reasoned about despite their rarity.
For example, you listed as some examples of exceptions that are not error: Divide by zero, failures of memory allocation.
Let's say you write a calculator GUI and I try to divide by zero in it. Exception or error?
If I applied your advise, I would have to deem this an exception, and just "just note down what happened" or "die quickly" (your words)! That is quite obviously wrong, in this example.
The correct answer would be to feed some kind of error code to the calculator's output data path, which would eventually be displayed on the screen.
Sure, I'm sure you'll come back and say now "well it depends then, and whether you consider it an exception or an error depends on the application". If you say that, you are ceding my point, because that is precisely what an 'error' (not an exception) is: a condition that may in some cases be fatal to the application, and in some cases be a part of ordinary runtime behavior.
I disagree with your case, this is clearly an error because it should never get to dividing by 0.
You MUST validate user input and user input validation failures are a class of errors not exceptions.
For example look at java (which uses the opposite terminology but exact same concept). Checked exceptions are expected state that should be handled gracefully, unchecked exceptions and errors are unexpected and the handling of them is generally let someone know what happened and exit quickly.
> I disagree with your case, this is clearly an error because it should never get to dividing by 0. You MUST validate user input and user input validation failures are a class of errors not exceptions.
This is bizarre: it sounds like you're disagreeing with me, by agreeing with me (that this is an error, not an exception)! For the confused (myself included), please realize that lost953's argument is considered "begging the question" (a logical fallacy [1]): You're using your a-priori assumption that 'divide by zero' is an exception, to argue that what we really should be doing here is add an if-guard before the "actual divide" occurs, so we can return an error, instead of an exception!
Otherwise, thank you for conceding that exceptions are just a category of error :)
No you misread my refutation, I make no claims of the errorness or exceptionness of dividing by 0, merely that your proposed example doesn't support your claim that it depends on the nature of the application. I claim that unchecked user input falls into the category of 'error' and that clearly is what has occurred in your proposed example.
I understand them to be saying that if execution is supposed to be halted before the division occurs, then whether the division is an exception or an error is a moot point.
I think the point the GP is making is that your calculator might have a function like like
calc_div(num: Number, den: Number) -> (Error | Number):
if den == 0:
return Error("division by zero")
else
return num / den
Now this function pre-validates the data. But it might be used as part of a much larger system. Should that system be programmed to know that `den` should always be non-zero and pre-pre-validate accordingly? Or else should it leave "calc_div" to be the expert on the rules for division?
If you take the latter approach, then the caller has to have a way of accepting the error gracefully, as a normal thing that might happen. And thus we have a div0 that is an error rather than an exception.
Ah, but floating-point math is such fun! Here is how it might work...
den is a tiny non-zero value in an 80-bit register. It gets spilled to a 64-bit slot on the stack, rounding it to zero, while another copy of it remains in an 80-bit register. The 80-bit version is compared against zero, and is non-zero. The 64-bit version, which is zero, gets used for the division.
It is fully standards-compliant for a C compiler to do this. Some languages may specify otherwise, but often the behavior is unspecified or is implicitly the same as C.
For many use cases, it a bad idea for a calculator program to use floating point, rather some more exact representation.
However if you do use floating point, then the kind of dangers you point out make my point even stronger. You could conceivably embed the `calc_div` function in a larger system that knew about pre-validating for div0. But if you want to deal with all possible sources of FP weirdness when doing division, then you really need to concentrate it in the "division expert": i.e. have calc_div pre validate all that stuff, and have its caller accept that errors are a normal result.
“Divide by zero” is an exception in the math library. But a GUI calculator shouldn’t pass all input to the math library without parsing it first, and figuring out what the user wants and if the user has entered a valid statement.
One guideline I follow for choosing between error codes and exceptions is “can the immediate caller do anything about this issue?” Even if the calculator does feed user input directly into the math library, the calculator can’t do anything sensible with an invalid statement. The calculator will have to “go up a level” and ask the user for help.
Disagree with validating that sort of input in code you (the non-framework/BCL author) write; let the operators, functions, etc., do their own work of deciding if their operands are acceptable. Otherwise, where does it end--do you pre-test two integers to verify their product won't overflow the type declared for their product? I think you gotta let the exception happen.
No: as I said above, an unhandled error generates an exception. But you shouldn't be able to have an unhandled error—good type systems prevent you from compiling such code, by treating all the errors that a function can return as cases you are required to handle at the call-site. Maybe generically with a default case, but you've still gotta handle them.
Dividing by zero is an exceptional situation (at the level of the processor, even!), rather than an error, because most divisions have known, nonzero divisors; certainly, all intentional divisions do. If you do end up telling the processor to divide by zero, it is assumed that you fucked up and didn't write your program right, because user-supplied divisors are a very rare use-case compared to known-domain divisiors, and a known-domain divisor being zero is indeed programmer error.
But, even if cases where the user controls the divisor are comparatively rare, they do exist. So, even if the processor isn't going to help you, why not have the compiler generate a check for them, such that integer division would be an (Either Integer Error) kind of operation? Well—performance.
Integer division—in languages where it generates an exception—is a low-level primitive. It's meant to be fast. (In fact, all integer operations in such languages are meant to be fast single-CPU-instruction-equivalent primitives; this is also why e.g. overflow isn't checked on integer addition.) Compilers for low-level languages are expected by their users to not generate any protective code for these primitives, because that protective code would get in the way of using the primitives at the highest efficiency possible. 99% of the time, the domains of these operations are fixed at the business level, such that these errors cannot occur. And, the other 1% of the time [like when writing a calculator program], the users of these languages use "safe math" libraries built on top of these primitive operations, rather than the primitive operations themselves.
Let me put this another way, with a concrete example.
In Rust, in safe code, you can't dereference arbitrary pointers, only opaque abstracted pointers (references) given to you by the runtime, that are guaranteed by the compiler to have non-null [direct] contents. You can make the inner type of a RefCell an Option<Foo> instead of just a Foo, and then it can be null... but this kind of null is an instantiation of the Maybe monadic sum-type, and so the compiler can notice when you aren't checking for its existence and stop you.
But in Rust, in unsafe code, you can dereference an arbitrary pointer, and the result can be null. What should happen when you do so? Well, that's an exceptional situation. You asked for unsafety, and then you did the stupid thing you weren't supposed to do. There's nothing that can really save your code now.
Before the invention of "exceptions", we just called these types of errors faults. Protection faults, for example. Your code would just be killed, without the ability to do anything, because it just did something that broke the semantics of the abstract machine (the process) that the OS had the program boxed up in. The OS might be kind enough to core-dump your process's memory in the process of killing it.
Exceptions are still faults. They just let you do arbitrary other stuff as your process comes tumbling down. There's nothing you, or the runtime, can do to "save" your process, when the "error" in question is precisely that you first asked your compiler for unsafety—asked it to not prevent your code from compiling if it does something invalid—and then you went ahead and used that unsafety to do something invalid.
In Erlang land, we have a third thing: exits. Exits are faults on a small scale—they kill the actor-process that generates them, and then spread across its linked siblings and parents to kill those too, until/unless one of them is "trapping exits", at which point that actor-process will receive the exit as an informational message from the runtime rather than just dying. Most actor-processes don't trap exits; and, in fact, you can't trap an exit generated by your own actor-process, only an exit "echoed" to your actor-process from below/around you. And the only processes that do trap exits, don't attempt to "save" the actor-process that is dying. Unlike with exception-unwinding, by the time an exit is "heard" by another actor-process, the actor-process that emitted the exit is already dead. The point of trapping exits is to decide what to do after the actor-process dies—for example, starting another one of the same actor-process to replace it.
(As it happens, POSIX exit statuses fit this concept as well, though semantics like bash scripts not being `set -e` by default kind of screws that up.)
Exceptions have their place—they describe a fault-in-progress, and let arbitrary things happen as a fault causes a stack to unwind, before the process dies. Exits also have their place—they let processes react to other processes having faulted. And errors have their place—they're a branch of the Either monad that a compiler can force your code to handle.
Personally, I don't see what confusion there is between any of these. They're roughly orthogonal.
And none of them represents "something bad happened from this function call. Somebody up there on the stack, help me recover, please!" (Those would be Lisp conditions, but nobody uses those.)
If you think of it more like “how do I want to handle something bad happening?” instead of “what category does this fall under?” then I believe electrograv‘s point of not being clear-cut becomes more clear.
For example in rust most code that can fail will return a Result and thus the compiler forces you to handle that. However, that code can just as easily panic and behave like an uncaught exception would (thread exiting). An example would be the division operator and the array index operator. Both division-by-zero and out-of-bounds errors can certainly be handled by using a Result but in this case the Rust developers made a decision to use panic. Are these both exceptions bc they are handled like a typical uncaught exception or are they errors bc it’s conceivable to handle them just like a failed file open?
(Those would be Lisp conditions, but nobody uses those.)
Off-topic, but why does nobody use those? The idea of "I have an exception, I'm the best place to act on it but the worst place to decide what action to take, request for advice" sounds good, at least
> Before the invention of "exceptions", we just called these types of errors faults. Protection faults, for example. Your code would just be killed, without the ability to do anything, because it just did something that broke the semantics of the abstract machine (the process) that the OS had the program boxed up in. The OS might be kind enough to core-dump your process's memory in the process of killing it.
Does your definition of a “fault” mandate that the process be killed? I’m sure there are architectures where forcing this to occur would require checks.
A "fault" is usually a term for a trap/kernel-mode interrupt that results from a check at the hardware level, built into an instruction. For example, a protection fault occurs when the processor attempts to access a memory address that isn't currently mapped.
There might exist ISAs without any equivalent to faults—but I would guess that the semantics of any "faultable"-instruction-equivalent on such an ISA, would 1. actually take the slow path and use a flag register to return the status of the execution; and 2. would thus require C and C-like languages to generate shim code that e.g. checks for that flag result on every use of the dereference operator, in order to generate a userland-simulated "fault"-equivalent (i.e. an abort(3)). This is because these languages don't have any way to capture or represent the potential flag-result return value of these operations. Low-level languages expect dereferencing to raise exceptions, not return errors. There's no place for the errors to flow into.
The reason I’m asking is because we seem to be a in a conversation where we are redefining words and tying them to abstract concepts, so I wasn’t sure if by “fault” you meant “this means the processor literally faults” or “I’m going to call a fault the termination of a process when it does something illegal, named because that’s how processors usually work today”. From your response, it seems like you’ve taken the latter interpretation.
> Low-level languages expect dereferencing to raise exceptions, not return errors.
Are you talking about C? Because it doesn’t actually define anything in this case. Sure, on most OS/architecture combinations, it will trigger a memory protection violation, but this is not guaranteed and I’m sure that in these cases the compiler doesn’t literally insert checks before dereferencing anything to guarantee failure.
Yeah, the "C abstract machine" that most C-like languages rely on by the fact of relying on libraries like libc.
> Because it doesn’t actually define anything in this case.
To be clear, when I said expect, I meant expect. These languages expect invalid dereferences to fault—i.e. to cause control flow to be taken out of the program's hands. Which is to say, they expect a generated dereferencing instruction that returns to have put the system into a valid state.
But in practice, what that means is that compilers just expect dereferences to always be valid (because any dereference that returns is valid.) So they don't have to generate any check for null before doing a dereference; and they don't have to worry about having any way to represent the result of a null dereference.
Another way to say that is that dereferencing null pointers is implicit undefined behavior. There's no part of the spec that covers what would happen if you dereferenced a null pointer, because in the model as the spec lays it out, dereferencing is a happy, clean little operation that never goes wrong. (i.e. "The MMU might cause a CPU fault? Who cares! My program didn't cause that fault; it was the MMU's choice to decline to read from $0. Other MMUs could totally allow that, and then LEA would be defined for the entire domain.") Same goes for reading from unmapped regions—"it's the MMU's fault; it could have just as well returned 0, after all."
> I’m sure that in these cases the compiler doesn’t literally insert checks
Yep, you're right. Again, it's because this is what these languages expect. It's not quite the same as undefined behavior; it's that the "C abstract-machine evaluation model" was defined to assume that architectures will always have their IDIV-equivalent instruction fault if invalid. If it returns a flagged value on some arch instead, that's something the C evaluation model is not prepared to deal with.
(Technically, I believe that you'd say that a C compiler "cannot be written" for such an arch while retaining conformance to any existing C standard, and for such an arch to gain a conformant C compiler, a new C standard draft would have to be written that specifies an evaluation model for such architectures—as then the compiler could at least be conformant to that.)
Which is helpful to know: just compiling C for such an arch at all, is undefined behavior, no matter what you write ;)
I’m not sure I follow your description of undefined behavior, which seems to me to deviate from the standard’s? Correct me if I’m wrong, but to me it seems like you’re saying something along the lines of “the C standard ‘expects’ that null dereferences fault, and ‘assumes’ division by zero to trap; it is impossible to write a standards compliant compiler for architectures where this is not true”. If so, let me know an I’ll explain why I disagree; otherwise, it would be nice if you could clarify what you actually meant.
The error bubbling up is a good thing. It at least allows you to find missing error checks in the underlying code. Good luck trying to find an unchecked return code. It seems you mix up problems innate to exceptions (like messing with the control flow) with user problems (unchecked errors/exceptions).
> Good luck trying to find an unchecked return code.
We're talking about alternate programming languages that handle errors vs exceptions differently, so it's only fair if we consider a language designed from the start not to need exceptions.
So let's take Rust, for example: In Rust, it's not a matter of luck at all: You declare whether your return code may be ignored; if not, it's a compile error not to use it.
Fair enough, error checks should always be enforced. I think errors and exceptions are both valid concepts that solve overlapping problem areas. Java has checked exceptions, so you can statically enforce exception handling.
Not even used for the C standard library and that would lead to warning spam as every printf call could fail and if there is anything I have never seen it is C code checking the return value of printf.
You would only mark functions where it’s a bad idea to return the error code that way. Printf isn’t a function like that, and fprintf probably isn’t (use case of printing to stderr), but fwrite is.
Nicely and clearly put. Doesn't really matter if it is merely a convenience as @electrograv said, i find this a very pragmatic way of looking and handling at the matter.
Using exceptions isn't "deferring technical debt".
Whether you use exceptions or not, you have to do all these things: detect errors, unwind the stack to a place where the error can be dealt with, and clean up resources during the stack unwind.
Exceptions are a control flow tool that simplify these things, nothing more or less.
Exceptions-as-alternate-control-flow is a paradigm that shouldn't have become mainstream in the first place. Using Exceptions (in effect forcing the addition of a new control flow path) unless where absolutely necessary has hurt the field, it's just like null. I much prefer the errors as values, and I think people are coming around to this point of view more and more recently, as languages are being retrofitted with type systems that are good enough to cope.
When I first saw Optional in Java (long before I'd ever done any meaningful work with languages like Haskell or Rust), I thought it was weird how it seemed to seep everywhere and be a good idea to use everywhere. This seemed off/weird to me at the time, but now I recognize it as the Blub paradox[0] (I'm not a pg worshipper but this is one of the most insightful things I've read of his IMO). The way I was thinking was wrong, at least from an overly pedantic sort of view -- failure is everywhere in Java due to nullable types, people have just been conditioned to pretend it isn't.
Nowadays I don't choose languages for software I write with nullable types not checked by some compiler -- so using Typescript when I do JS, or using Haskell or Rust.
I have always heard that "Exceptions" should be "exceptional". Meaning that most expected errors (eg: getting a non 200 response to an HTTP call, having a file not found when opening one, ...) should not be handled in exceptions, but in regular if...else blocks instead.
Exceptions are great for exceptional stuff we really could not have expected (or just don't want to deal with so we want to cleanup nicely), but they tend to be overused for "anything that is not the expected result.
On the other hand, deferring non expected path to later using exception like you clearly state sometimes actually is good thing as it speeds up development time significantly. Depending on the project, you may actually really not care, and having that power in your hand in invaluable.
I agree with this. Error handling code and exceptions are not mutually exclusive, and both have their benefits and drawbacks. Checking return codes in actually exceptional circumstances makes the code base almost unreadable, and exceptions work amazingly for this. On the other hand, using exceptions for common errors is both inefficient and ugly.
As an example of useful exceptions: malloc errors. In C, handling malloc failures is a nightmare. So much so that very few programs actually do it properly. The reason for that is that not only do you need to check the return code of malloc, you need to check the return code of every single function in your entire program that ever calls a malloc anywhere down the line.
So while this piece of code might be nice if you see it in one or two places:
int rc = fx ();
if (rc != 0) {
handle_error ();
}
Properly handling malloc errors means every single function call becomes a four line monstrosity, basically blowing up your code base by factor four and making the code much more difficult to read. Not to mention how insanely error prone it is to have to check the return code of every single function. When doing the wrong thing is so much easier than doing the correct thing, most people will do the wrong thing.
This is an API design issue. The proper solution is to provide two versions of malloc or equivalent - one that doesn't have any error code, and simply panics on failure to allocate, and another that provides a way to recover. A typical app would then mostly use the first version, and very occasionally the second when it anticipates that allocation might be so large that it could fail even in a healthy environment (e.g. loading an entire file into memory).
Panicing is not handling anything, it is just crashing the program. Unless you can catch the panic, in which case it's just another name for an exception. Not handling failed allocations except for on large allocations is just asking for rare crashes when one of the "unlikely to fail" allocations fails. Neither of these solve the problem in a robust way. Handling this properly is a language design issue. This is just applying a band-aid.
This is just recognizing the status quo. Pretty much no desktop or mobile or web software is handling "unlikely to fail" allocations. And how exactly do you expect them to handle it? If, say, it's a desktop app that's rendering a widget, and as part of that it needs to allocate a string, and that fails - how does it recover?
Panic on OOM is perfectly reasonable for most.
And yes, panics shouldn't be catchable. That's the whole point.
If you use a language designed from the start not to need exceptions, this problem is solved: The code in fact looks very much like code that uses checked exceptions (in fact, another commenter even mentioned Rust’s type system is isomorphic to checked exceptions).
I have no problem with checked exceptions. Unchecked exceptions is another story, and forces you to “catch all” everywhere (most libraries don’t document every single possible exception of every single method).
In retrospect I don’t even know why everyone responded to my original post about focusing on software error handling as if I was attacking exceptions. I think unchecked exceptions and unchecked null pointers are bad, but that’s about it; and that’s not even what my top post was about.
I would rather get rid of the term "exception" at all, and instead talk of recoverable errors (which should be properly reflected in the type system, as in e.g. Rust), and contract violations (which should result in an immediate panic).
> On the other hand, deferring non expected path to later using exception like you clearly state sometimes actually is good thing as it speeds up development time significantly.
Personally, I like the idea of Java's "checked" exceptions, particularly when your software involves lots of layers that need to use one-another appropriately, including adapting to error situations.
If you go "quick and dirty" with unchecked exceptions, you have the option to change them to checked ones later. When you do that a lot of stuff will "break", but really that will just be the compiler walking you through all the paths the exception could take, asking you to make a decision at each spot about what you want to do. (Pass the exception up the stack unchanged, catch it and wrap it and throw the wrapper, catch and recover, catch and ignore, etc.)
My argument: 'Exceptions should be exceptional', because exceptions are really just errors, that happen to be exceptional (rare)!
The reason exceptions tend to be overused is because the line between error and exception is blurry -- and it's blurry precisely because an 'exception' is really just a subset of errors. Unfortunately, most languages do not treat exceptions as a subset of an error, but as a completely disjoint/orthogonal thing, and that's the problem!
If we transitioned to languages that handle these concepts in a unified way (and ones that don't allow unchecked exceptions), this isn't a problem at all, and we can all happily write much more inherently reliable software.
> If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
Clarification please--are you suggesting the programmer can anticipate all reasonably likely error conditions and implement handlers for each?
I lean towards treating recoverable errors as uncommon, preferring a log->retry->give up strategy in most cases. The biggest sin is often in obfuscating the source error or prescribing a bogus resolution. Exceptions, while not perfect, remain a pretty good way to convey what was going on when things went sour.
> Clarification please--are you suggesting the programmer can anticipate all reasonably likely error conditions and implement handlers for each?
Absolutely, insofar as it is possible for a programmer to write their application in one of the several existing programming languages that can guarantee at compile time that there is no undefined behavior / exceptions / crashes / memory corruption. This concept is sometimes referred to as a "total function": a function that is defined for all possible input values, and in a language for which there is no way to invoke that function on an input not in its statically-declared domain.
Now, I'm not saying this is possible for all programs, particularly for certain kinds of I/O, but with a reasonable amount of error handling code and robust interfaces between that I/O device and your code, it's usually possible to get pretty close to perfection there too.
I'm also not saying it's easy. But I think this is precisely the kind of "hard" the software engineering industry needs more of right now; and languages like Rust and Zig and even Spark (Ada verifier) are a wonderfully refreshing move in that direction. Note: Not all of these languages I mention are 100% perfectionist either! What's important is they move significantly closer to perfection, and away from this 'cowboy coding' mentality of not really caring about errors/exceptions until they bite you.
Really puzzled that you have conflated exceptions ("'x' went wrong, here's the stack") with undefined behavior, crashes and memory corruption.
It seems as though you're describing how the world ought to be, and maybe could be, for the writing of pure functions, procedures with provably no side effects, and the like. Well, OK.
However, imagine that you're writing a few bytes to disk. Maybe the device runs out of space halfway through the write, the filesystem was mounted readonly, or the filesystem doesn't like the filename you asked for due to length or case-insensitive overlap with an existing name, or the controller driver glitches out, or permissions are wrong, etc., etc. You cannot anticipate all of these and implement recovery, aside from informing the caller what went wrong, and giving an opportunity to somehow correct the problem elsewhere and retry. Well now you're in the business of not really recovering from the fault, but instead doing something the same or nearly the same as raising an exception and printing the stacktrace.
TBH I have misgivings about even writing up that last paragraph because the alternatives really strain credibility.
I don't think the parent was suggesting that there's anything wrong with what you wrote in that paragraph. At least IMO that counts as handling the error, vs. just doing nothing and assuming everything is ok. If you have to kick it back to the user and say "sorry, XYZ happened, and there's nothing I can do about it", that absolutely counts as handling the error. Not handling it would be just assuming the file got written properly to disk and then later having to deal with corrupt data.
You could also argue that relying on "total functions" is an antipattern because it doesn't account for "undefined behavior / exceptions / crashes / memory corruption" and leads programmers to overreliance on the compiler. We can throw race conditions and deadlocks in there too, since very often it will be difficult for a compiler to detect those at compile time. A PL that gracefully handles programming errors as well as exceptions, memory corruptions, etc, emitting a logged problem and continue to chug along will let the programmer decide how bad an error is, with high uptime, and make a business decision about whether or not it's worth fixing at the time.
I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
If you spend more than half your time in error handling, you have an architectural design issue with your code. If you need to be that vigilant then it's too easy for a junior hire to make a mistake that makes your code unreliable. Design out pitfalls. Reduce the amount of code in your system where you need that level of vigilance, and you'll no longer need to spend 50+% of your time on error handling.
Designing out pitfalls is precisely what I’m talking about and arguing for — and that takes time (in an application of any sophistication), either because you’re using a language (like Rust) whose compiler nitpicks your code in ways that 99% other languages would just accept without complaint, or because you’re being equivalently paranoid in your design of every single core data type, interface, platform service, etc.
There may also be some disconnect here in the type of code we’re talking about. I’m talking about work on code that millions of people are relying on to be 100% rock solid. I don’t care if you’re junior or senior: when writing code at this standard of quality, caution and rigor are always part of the process (both by the author, and the review and testing process).
If you can write absolutely rock solid, 99.9999% bug-free code that handles every possible error case gracefully, while spending more than 50% of your total coding time spent typing new code (which apparently has no error handling) literally as fast as you can type, well... WOW!! Consider me impressed; It seems we all have a lot to learn from you. If so, I genuinely would love to learn more of this seemingly-magical process where you can write perfect code at maximum speed, while also not having to think about edge cases or other errors.
Anyways, back to reality:
The fact that so much of this caution associated with writing bug-free code is loaded onto human judgement right now is exactly why I’m such a strong advocate for languages like Rust and Zig that aim to move much of this cognitive burden into the compiler.
For example, let’s talk about designing out pitfalls: say I create an immutable data structure in C++ with a really efficient implementation (zero-cost copies, automatic memory sharing) that is virtually foolproof when accessed from multiple threads, used in computations, etc. No matter how foolproof I make this C++ class, I still can’t stop your “junior dev” from stomping over the end of another array into my class data, or using dangling pointers, etc. etc. etc.
We can enforce “safe” classes wherever possible, but that also runs up against the wall of reality when interoperating with other C++ code that has a different idea of what constitutes that “ideal C++ subset”.
_I don’t care if you’re junior or senior: when writing code at this standard of quality, caution and rigor are always part of the process_
There are (at least) three stages for a program: make it work, make it good, make it fast. Most programmers I know (aka juniors) are happy to get the first stage done, for a value of done (it worked on my machine, when I tested it with this particular sequence). After many, many years of programming (aka senior), I'm proud to say that I usually take care of the second stage, and sometimes even the third.
The whole point of the junior / senior distinction is that seniors have been burned more times by some things so they insist on processes like always having source control, at least trying to have automated tests and so on - processes that a lot of juniors find irrelevant to getting things to work in the first place.
What kind of "junior" are we talking about here ?? The average comp. sci student will insist on source control. Are you hiring high schoolers or what ?
> If you spend more than half your time in error handling, you have an architectural design issue with your code.
Ha, I once wrote a lock_file(...) function in 30 minutes. Then spent the next two years getting it to work on various platforms, on network drives, dealing with re-entrance, and so many other corner-cases you couldn't believe. The initial 10 line function turned into a whole module.
I'm not sure that counts as error handling as much as handling things you don't expect.
There's no such thing as a language with no exceptions.
There are only languages where exceptions need to be hand-rolled in an ad-hoc fashion.
This ad-hoc approach does not work. It gives you tiny benefit of not needing to learn a language feature. Meanwhile, ad-hoc exceptions mean that you lose all sense of modularity in your program. Real programs are composed of dozens of modules at various levels of abstraction, communicating together over multiple processes and threads. Ad-hoc exceptions in a real program in practice means 'just crash the whole thing, yolo'.
Well no, every time you have a chain of functions in your call stack that all do
if(f(blah) != E_OK) {
return E_WHATEVER;
}
(which is veeeery common in C) you are reimplementing exceptions by hand (and with less performance in the no-error case since you still pay for the branches, while a C++ code with exceptions would just be a sequence of function calls without branches in that case)
I wouldn't disagree with you. The OP said that programming languages w/o exceptions aren't successful. C doesn't use exceptions, it has it's own way of dealing with similar issues, and yet it's quite successful.
>I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
That is a big if. Most projects don't mature that much. And for those that do, regretting things after the project has matured is a "nice problem to have". E.g. even if Zuckerberg has regretted some bad early FB design (let's say regarding error handling), he's is still a billionaire.
Second, you can handle or ignore errors with exceptions just as well as with any other mechanism (multiple return values, optionals, etc).
As someone who has spent years writing motion control code, I agree wholeheartedly.
Thinking of code I've written to move an object from one physical position to another and in pretty much every case, the error handling and recovery paths are the bulk of the code.
Errors in motion systems are not rare events. Things get sticky, wires break, sensors get clogged with dirt, parts wear out and break off and throughout it all, this thing has to keep moving from A to B reliably or at least clearly indicate when it's failed unrecoverably before something worse happens.
I can follow your premise that not enough time and effort ia out towards making sure our programs are correct and have low defect. I'm not sure I follow your jump to claim Rust and Zig solves that problem.
But I guess time will tell, when there will be as program running in those as there are C/C++ programs, we'll see if things are any better or not.
Back in the day I used to work on low level telecommunications code. Often there the happy path was treated as (almost) an afterthought and handling all the error paths was the focus of the work. It was possible to work that way using C or in the various assembly languages we used.
I think exceptions are kind of cursed by their name. To treat exceptions correctly you have to constantly keep in mind that they are misnamed. And you have to deal with type systems that do not treat them as precisely as they treat return values. And on top of that you have to coexist with people who impose all kinds of semantic limitations on their mental model of exceptions, such as that they have to be "exceptional." Exceptions have to be treated as happening all the time, and in strongly typed languages (such as Scala, my hammer) you have to keep in mind that the type system has this huge loophole built in that is very important and that the type system gives you very little help with.
(The most obvious example that utterly confounds semantic limitations on exceptions is opening a file. Programmers accustomed to working on a certain kind of system find it quite natural to regard a missing file as "exceptional", even fatal — for them a missing file means the install or even the very build is corrupted. Programmers who work on a different kind of software may regard missing files as completely routine, maybe a commonplace result of user error. If these two groups of programmers believe exceptions are "exceptional" they will never agree on the contract for opening a file. Another example is deciding whether something that happens 0.0001% of the time is exceptional. Some programmers will regard that as exceptional while others will consider it utterly routine and regard it as a failure of discipline to believe otherwise.)
(The logical consequence of insisting on "exceptionality" is that you need two sets of basic libraries for everything and may need to mass refactor your code from one to the other as your use cases change. This is a needless imposition that offers no recompense for its offensiveness to good taste and good sense.)
The great merit of exceptions is that they remove boilerplate and make it trivial (nothing is more trivial and readable than no code) to express "I am not handling this, someone else must" which makes it quite easy to e.g. signal "400 Bad Request" from deep within some code that parses a request entity.
Personally, I think that for now it is best to prefer exceptions for writing lots of useful code quickly and concisely and to prefer strongly-typed, expressively-typed return values for achieving a higher level of care and reliability. But I look forward to a better linguistic solution that combines the virtues of both these approaches — and I have to admit that in my ignorance of many important languages I may be overlooking an already existing solution. I am reminded that in the early 2000s I would have relegated all static typing to the "verbose boring highest reliability required" category and then type inference and other ergonomic advances converted me to statically typed languages such as Scala as the best way to pump out lots of valuable working code. I'm looking forward to a linguistic solution to this problem.
It's also more flexible and composable, since checked exceptions aren't quite a proper part of the type system. Consider the case of a higher-order function that wants to derive its exception specification from the function it wraps.
You also need union types. But yes, you can make it work (the original lambda proposal for Java tried to do that). Of course, once you push it to the point where it does, then it is practically indistinguishable from having a single return value that is a discriminated union.
The consensus we have reached at our company is as follows:
1. If you plan to write a software library [0] to be embedded directly onto foreign piece of code (i.e a host application), then go blindly with ANSI C or C99 at last resort. Not only writing bindings for your code should be straightforward but you will also benefit from the portability of C that make your code virtually available on every platform or architecture out there. Remember write once, run everywhere. Well, C fits exactly onto this category.
2. If on the other side you plan to write a standalone software such as a Desktop app, a network server, a game, etc. then modern C++ should be your first choice. You'll largely benefit from shared/unique pointers, built-in threading, lambdas, high performance data structures with relative portability accros modern systems. C++11/14/17 is a joy to program with once you master the basics.
For 1. I'd still recommend using C++ (or D or Rust) and wrapping it in a C interface because these languages are just better at handling complexity compared to C.
Usually one has some idea what the target platforms are, and writing C code that runs everywhere is pretty hard anyway.
Unless you're targeting really deep embedded stuff, C++ is also available on virtually every platform and architecture out there.
And if you are targeting the kind of platform for which there's no C++ compiler, its C implementation is likely to be very idiosyncratic as well, and portable conforming C code might not even compile on it.
If C++ has advantages over C, then I don’t see a reason to limit yourself to C in libraries (unless those advantages aren’t helpful for libraries).
I do agree that writing libraries is fundamentally different (and fundamentally harder) than writing apps. I still don’t feel comfortable passing smart pointers across a C++ API (in or out), so I’m fine with a rule that the API should generally be C-friendly, but you give up a lot when you limit yourself to C, and some of what you give up would be very useful inside the library.
That the author didn't understand how to write robust C++ by 2012 (after 5 years) without constraining themselves unnecessarily to C is not a compelling reason to prefer C over C++.
"Doctor it hurts when I do this." Well stop doing that then. Learn what works and what doesn't, but don't throw the baby out with the bathwater.
Yeah it seems... odd. I fully get preferring return codes to exceptions, but you can still just do that in C++? Heck, you can trivially do a type-safe union of return value or error code in the spirit of Rust in C++.
And you can do things like enforce that the return value is used by checking in the destructor if the return value was unwrapped.
Similarly for constructors if you have an object that can fail construction instead of doing what the author suggested of
class foo {
public:
foo ();
int init ();
...
};
You could instead do something better like:
class foo {
private:
foo ();
int init ();
...
public:
static std::optional<std::unique_ptr<Foo>> create();
};
This is a well-established pattern in languages like Java, for example, to do exactly what the author wants - a constructor that can fail without forcing callers to just know they need to try/catch.
Of course you can do various things in various languages, but good luck getting any adoption of a library that goes against the language idioms in its language. You expect certain kind of API from C libs, certain one from C++, and different one from Java packages.
If C idioms work better in your project, just go for C. You get other advantages from that, like faster compile times and being independent from C++ stdlib, which can be messy at times (not even speaking about Boost). You don't have to stay with C++ just because somebody else (or even you) thinks that it's generally a better language.
Roughly ~50% of the C++ ecosystem compiles with -fno-exceptions[1]. So using return values instead of exceptions is not against the language's idioms at all. It's one of the reasons std::nothrow exists, after all.
Similarly while exceptions are part of the Java idioms, it does after all have checked exceptions even, it's incredibly common to not use that idiom on constructors, where it's weird. Just because a thing is an idiom in some cases doesn't mean it's the expected idiom in ALL cases.
And if an idiom is dumb it doesn't mean you should just do it to follow along with the dumb, nor should you go to a language where you're forced to be less safe & less clear just because "but mah idioms". Be the change you want to see.
To be fair to the author, std::optional was added in C++17, which came after this blog post was written. I agree, maybe if he had written it today his views would be different.
And yet boost optional has existed for a good half of forever. C++ programmers have been writing they're own versions (maybe, fallible, etc) for the other half.
There are certain widely accepted norms on how to handle errors in C. It is best to restrain ourselves to "prior art" so that your code is immediately comfortable to read and use for a seasoned C dev.
To list a few: return codes, errno and longjump
Don't use setjmp/longjmp unless you can guarantee that you own all frames between one and the other (i.e. don't use it as a public error reporting API in a library, for example). They don't play well with C++, and really anything else that needs to unwind the stack. Those who write R extensions in C++ know how messy it can be.
Your example doesn't tell me which I have. It's assuming that err = 0 means success, but that's an assumption not a contract of the type.
By comparison some something like https://github.com/oktal/result will tell you if it's a success or error without any magic error codes that actually mean success.
You could do this with a type bit + union in C, it's just more painful without templates
That requires 1 bit of information, and there is often a way of embedding that bit in the value itself; but the general technique of representing alternatives is called “a tagged union.” (You can also hear the fancy terms “the product type” for the struct and “the sum type” for the union.)
Have it return by value in an optional then if that's what you want. You're not forced to do it on the heap. Or if for some reason you need to be able to pick you can do the whole templated allocator song & dance.
Writing C++ like Java (or any other language for that matter) won't help anyone. The code will turn out non-idiomatic and weird for any couple of C++-eyeballs.
Especially since the GoF Design Patterns, which are often cited as the main reason for Java being verbose, were described in the original book using C++ (and smalltalk) examples.
Just because they may be overused in Java doesn't mean people shouldn't use them at all in other languages.
They have originated from C++, fine, but it is a different language, different ecosystem, different community, different ideology and -- different idioms
The author is not an unknown entity. ZeroMQ is one of the highest performing brokerless MQs, if not the highest. I do remember that the main pain points were wrt STL and not C++ (but it's been a while since I read that post.) Other people you might respect have tasted C++ and bailed on it (eg Linus Torvalds.) For me, going to a template-less world would be a nightmare.
My respect for Linus does not extend to his opinions on programing languages. I haven't found any of Linus's objections to C++ compelling. I strongly believe that a C++ kernel developed with the same care as Linux would be strictly more robust. There are simply more and better tools for constraints and engineered safety in C++.
Besides arguments about portability, C's only "advantage" vs C++ is a lack of features. This isn't an advantage for a serious engineering project, or really any project with code review.
And if you're looking for an example of a kernel being developed in C++, look at Zircon [1] (the kernel for Google's Fuchsia project). It's certainly past the "trivial complexity" phase, and doing just fine.
And of course, things like RAII are incredibly useful when managing resources, and templates for encapsulating generic data-structure behavior in an operating system.
So to me (I'm no C/C++/Kernel/etc. expert) it looks like they are writing the kernel mostly in C, with C++ goodies sprinkled in some places, where it's useful (ie: RAII for spinlock guard)
Lack of features can be a good thing. There will be no “language feature anxiety” for the people who is writing code. The code is easier to comprehend for new people who just joined an ongoing project, because there are no non-obvious execution flows. And C code can be compiled so much faster than C++ due to the language being simpler.
I usually spend the compilation time staring at code, only to realize, somewhere in the middle, that I have to abort the build because I must make another change in a large and complicated template in header file that is included almost everywhere. At times, I do miss C, especially its classical variant, where you often didn't even have to include anything and instead could just write the declaration of something you needed - right next to where you need it...
You can still use forward declarations. You can even use them in C++. But you have to be sure to get the signature exactly right (one of my favorite passages from “Design & Evolution of C++” involves Stroustrup’s discovery that a lot of programmers were cavalier about their forward declarations, e.g., using “...” as in any declaration where it would be syntactically valid).
> For me, going to a template-less world would be a nightmare.
I could do with just generics. I am honestly sold on the idea that OOP is flawed. I've tried to do "real" projects in C, but the continuous casting just felt like unnecessary ceremony.
> Other people you might respect have tasted C++ and bailed on it (eg Linus Torvalds.)
Sorry, Linus didn't say anything compelling against C++. He explained very clearly why C++ didn't fit with his own personal way to work, and that's fair.But his message did never contain anything more than this!
I get this point but if you’re using the STL then you’re going to have to deal with this stuff.
At one point hacking around these issues become a bigger cost than just using some C library
Though personally I think the abstraction ceiling on C is so low I can’t imagine that cost ever being that high, other people are more comfortable with macro-based APIs
I'd love a real world example as I've been using the STL for 14 years, and only thing I've worked around was std::map, which I implemented much like boost flat_map.
Yes, you need to be aware of object lifetimes in c++, but so do you in C. C++ makes it far easier to manage, though.
I agree. These days I can't even imagine programming in C++ without using STL. It still has some weird ergonomics at some places though. For example - instead of a simple vector.contains(something) we have to write this
std::vector<T> x_Vec;
if (!(x_Vec.find(another_x) == std::vector<T>::npos) {
//some code
}
The first snippet you wrote makes sense for string, but not for vector. For vector, you'd need to do:
if (x_Vec.find(another_x) != x_Vec.end()) {
//some code
}
The reason why vector doesn't have contains() is because it would be an O(N) operation for a vector. STL is generally designed in such a way that, when something needs to do an O(N) lookup, it has to deal with iterators, making the iteration happening in the background a bit more explicit.
But, for example, for std::set and map, you can just do:
if (a_set.count(x)) { ... }
It's not a good pattern for multiset and multimap, since it won't stop at the first occurrence. C++20 finally added contains() for that reason - but, again, only to associative containers.
Indeed! Which is why find() is not a member function on vector (so my snippet above is wrong - I just missed that part). It's a global function, so you need to do:
if (find(x, v.begin(), v.end()) != v.end()) ...
For maps and sets, find is a member function though. Same principle - if it is implemented in some optimized way, the class provides it directly, but not otherwise.
> "Doctor it hurts when I do this." Well stop doing that then.
A surprising amount of pain comes from cargo-culting/copy-pasting some behaviour because the "old way sucks" and the "new way rocks" (the opposite problem also happens, that it being stuck in old ways, or: "how do I lift this excavator? I want to dig" )
I'd suggest that the author understood it. The point he seems to be making is that writing such code in C++ was not that much better than in C. At least with regards to exceptions, boilerplate and such.
The article is mostly about exceptions, which is fair I guess.
However, by the time you write C code that handles _all_ errors (like exception handling would), you end up with code that does not look as nice as the examples given here. In production C code you often end up seeing a "goto fail" or "goto done" and then hand-written destruction of objects. To make that less error prone, you have to explicitly initialize everything in the beginning of the function. Oftentimes you even see the "goto fail" invoked via a macro.
Now, in my humble opinion, by this time you might as well have used C++. Persponally, I do like the regularity of such code when reading other people's code. Still. It's a lot of effort. And the benefits over exception handling are not so obvious to me. You end up restricting yourself in both cases.
Also, when considering whether constructors and destructors should throw exceptions, consider the array case. You instantiate an array of 100 foo. If constructor #42
throws, how do you know which ones have been initialized and which ones haven't? Even worse, if you delete[] an array and one of the destructors fails -- do you get memory leaks? Maybe the destructor was supposed to release a mutex and you can get deadlocks?
I find it very telling that both C and C++ (and most other languages for that matter) end up defining a "good subset" that you are supposed to stay inside. You would be forgiven for concluding that we don't know what we are talking about when we design new languages, and we should not hastily add new features lest we end up having to recommend against them in the future. Yet, many languages (I'm looking at C# and Javascript here) keep getting new features on a yearly basis.
I think we should be much more sceptical.
I also think this is a dilemma that is similar to the startup dilemma. It is a very risky proposition, so the people who end up doing it are not risk averse and tend towards the "hold my beer" part of the spectrum.
The one real reason to use C++ is for templates. Exceptions, object orientation, etc. is all fluff. C++ is far better captured as “C with metaprogramming” rather than “C with classes”, and the other features are the supporting cast for that, not the other way around.
RAII strictly for preventing leaks of resources is the lone thing I miss in C.
Constant folding via constexpr in C++ is much much better than C.
Templates help write generic code that results in specialized machine code; it may be hyper optimized at the cost of binary size. They make it harder to write code that at runtime is a little more type generic (if that makes sense). You can't always afford those additional copies generated. Not that it's a bad thing; just an observation as I write both strictly C and C++ at my day job.
Stealing the below; string handling is significantly safer with std::string. I'm not sold on std::string_view.
You can control code generation for templates by putting only the generic data structures in the header, and all function bodies in an implementation file. Then you explicitly instantiate the template for the types you want it for in that implementation file, and declare those instantiations as forward references in the header.
It's not pretty, but it's not hard either, and gives you full control over how many instances there are around. When C++ is used in a heavily constrained environment, that can come in handy.
The bug there is implicitly converting an r-value to a non owning reference. We wouldn't call the same semantics a bug with a 'char const*' to be fair. We would complain that string implicitly converts or reject the application code as buggy.
That being said, yes, string semantics are hard. Passing around copies of a string buffer (std::string) or calling to_string_view everywhere don't seem better.
C++ also has a fair number of built in STL smart-pointer types that do approximately the same thing as the "move/borrow" semantics which everyone gets excited about in Rust.
These alone are a worthwhile "dialect" on top of C, the use of which results in a far safer C; and—as far as I know—these smart-pointer types and their compile-time semantics would be impossible to implement without C++'s additional layered-on "C with classes" type system.
(It's clear that these smart-pointer types are a valuable addition to C, because they were independently implemented as part of all sorts of other C macro libraries in the early 90s. I think there were at least four C smart-pointer implementations that existed as part of various parts of Windows, for example.)
STL smart pointers are all fun and games until you start using threads, at which point you realize that automagically flipping the (now atomic) counter back and forth behind the scenes every time the pointer can't be safely passed as a reference is a performance disaster.
Manually referencing and de-referencing exactly when needed avoids most of this. Sometimes it's perfectly safe to pass the same reference through several layers without changing the number of references.
As an added bonus YOU get do decide which references, if any, need to support multiple threads.
Reference counted smart pointers are just one of the many smart pointer types. Atomically reference counted smart pointers are a smaller subset of those.
I think the GP was accusing you of "side tracking" by bringing up std::shared_ptr in the first place.
My post above was about how C++ smart-pointers compare favorably to Rust's oft-considered-"unique" ownership/borrowing system. The particular STL features that cause equivalent "compile-time magic" to happen to what you get from Rust's semantics, are those of std::uniq_ptr and std::move, not those of std::shared_ptr. So the value (or lack thereof) of std::shared_ptr, isn't really a knock against the value of the C++ STL smart-pointers collection as a whole—you can entirely ignore the existence of std::shared_ptr (I do!) and still think C++ STL smart-pointers are the cat's pyjamas.
True it requires classes as RAII is based on a destructor. However this doesn't require OOP design, which typically is associated with classes and objects.
You don't have to use inheritance, but you will certainly use object composition and encapsulation to manage the lifetimes of your data in a coherent way.
Umm..things like standard library strings, futures, threads, memory management via shared_ptr, unique_ptr, RAII etc are also other great reasons to use C++. Every time I attempt to code in C, its like my hands and legs are cut-off. I suppose some folks enjoy this.
The biggest problem with C isn't even it's lack of type safety, undefined behavior but that antiquated macro system which does more harm than anything.
Imagine if C had a solid AST-based macros system...
If you're interested in alternate languages along the lines of 'C with metaprogramming' (and without the bad stuff C++ forces you to deal with in some cases), you may enjoy taking a look at Zig (https://ziglang.org/) or Rust (+ RAII).
This is actually one of the strongest arguments against writing performance sensitive code in C++.
Because of operator overloading, I don't know if `A * B` is incurring a (nontrivial, arbitrary) function call. Maybe it's just normal scalar multiplication, maybe it's finding the dot product of two arbitrarily sized matrices.
Having used multiple maths libs over the year I have never been in a position when I did not know immediately what A * B does. Why are you writing it if you don't know what happens ? At which point in your life would you write A * B and not know if one of A or B is a matrix ?
You'll know what it does if you're the author. But code is read much more often than it's written, and it's easier to spot bugs and understand what each line of code does without operator overloading. Operator overloading obscures what the actual code is doing. Because of this, it's is generally not used in kernels, virtual machines, etc.
As an example, if I give you this line of C++ code, and no other information (which would require further effort to determine, e.g. the types of identifiers), can you tell me if it's a function call, performs I/O, or does dynamic memory allocation?
Now tell me if it involves a function call, performs I/O, or does dynamic memory allocation?
And, of course, things like macros and typeof are totally used in kernels, virtual machines, etc... The ultimate obfuscation is alive & well in those areas. So no, the potential to do terrible things in operator overloading is not even remotely a concern there. No more than anything else.
That's not equivalent C code - because there is no equivalent C code.
I can tell you, if I write this in C, it performs no I/O, memory allocation etc:
typeof(A) C = A + B;
I know for a fact that (modulo people doing incredibly stupid things with macros), that this is simple arithmetic addition. I don't know that for a fact in C++ - because of operator overloading.
> I know for a fact that (modulo people doing incredibly stupid things with macros), that this is simple arithmetic addition. I don't know that for a fact in C++ - because of operator overloading.
The only way to know for a fact what happens in either C or C++ is to know the types of A & B, which you (seemingly intentionally) omitted.
But if A & B are the types that can be added in C, then it's going to be literally identical in C++ with no chance of operator overloading. If A & B are not types that can be added in C, then you would see an add() function called instead and you'd be right back to not knowing what it does.
Yes, I am intentionally not looking at the types, because that's something that might not be incredibly apparent in C++ (or C).
We're violently agreeing at this point. Yes, in C, this problem does not exist, because operator overloading does not exist.
In C++, what `A+B` does is ambiguous without knowing 1) what are the types of A and B, and 2) what are all the possible `operator+` overloads that exist which may operate on A and B. It's a hidden complexity which doesn't make potentially expensive operations apparent, and kernels and VMs hate that kind of thing.
> because that's something that might not be incredibly apparent in C++ (or C).
Yes, it is. Extremely so.
> Yes, in C, this problem does not exist, because operator overloading does not exist.
Except it totally still does because you never know what function add does for random types. It's literally the same exact thing. Function calls might be expensive and they might not be.
> It's a hidden complexity which doesn't make potentially expensive operations apparent, and kernels and VMs hate that kind of thing.
No, it really isn't. The add overload operator is never non-obvious, and never actually does memory allocation, file io, or any of that other nonsense.
Can you be intentionally stupid about it? Yes, sure. Is that an actual concern that anyone working on a VM or kernel should have? No, not in the slightest. Completely nonsense.
Whilst I agree with you that this isn't the best use of exceptions, one reason to use them this way might be because exceptions (generally) don't cost anything on the success path. if statements do.
Of course exceptions cost a lot more on the failure path, but that's not generally the priority.
> local try/catch-es are examples of exception misuses.
Not in the context of RAII. It helps a lot with cleanup and ensuring nothing leaks from the snippet that caused the exception if any resources were acquired.
Whereas in C you'd have to make all that cleanup manually, which can be tedious and error prone.
maybe the thinking is that since library functions that you use can also throw exceptions, you should also do that in order to not mix error handling. I don't really get it either though. I just like C more because of its restrictions, and I consider restrictions a virtue if they point you in the right directions (e.g. good and predictable performance).
I expected this to be about ABI stability or something. Exceptions aren't a good reason because you don't have to use them. You can even use something very close to Rust's Result<> if you want. LLVM does this.
"But what about constructors??" you cry. Again you can do something similar to Rust - make the constructor very simple and private, and the have static make() methods that return a new object or an error.
IMO, in 2018, it is a mistake to rewrite nanomsg in C. Either Modern C++(17) or Rust should be used. The advantages in expressiveness and safety make a big difference.
We uses ZeroMQ on production as part of a message layer which provide very tight and predictable latencies sustaining more than 300K QPS across multiple services and across 3 data centers(GCP, AWS and private Datacenter). This is a stellar testament of how good ZeroMQ is as a mature solution.
I think the reason we are successful with our design is because ZeroMQ is just great for what it does.
It never crashed or gave problems to us, only issue we saw was because we did not understand how it worked internally.
Had it been written in C, we would have been able to hack it a little bit to our tastes but I am not complaining. I can understand why someone would use C instead of C++ for high performing libraries.
Oh on the subject of C:
This is what we have :- Main service which is written in C does around 8K QPS at peak hours(on a 2 core 4GB Amazon EC2 instance), so we have around 35-40 instances in the cloud.
The cost to run this service is low for us, we love it.:)
Biggest issue, people are bad at C and writing correct multithreaded code in C is an Art.
People mostly keep saying that:
1. C++ has at least everything C got, and "other features" so it cannot be worse.
2. C++ is safer than C.
Regarding 1.:
a. C99 and C++11 have diverged. For example, `struct xx x = {0}` has different meaning in those languages.
b. Lack of certain "features" is an advantage. See, C++ is a huge and complex language (r-values, x-values, exception safety, templates, sfinae and so on and so forth). Yes, you can restrain yourself from using them. But can you restrain others working on your codebase, now and in the future effectively? Good luck writing and maintaining code style guidelines..
2. Well, yes, by using smart pointers, STL, RAII, C++ is safer than C for small, fresh codebases. But remember: C++ was designed for backward compatibility with C. It's full of undefined behaviour and pointer arithmetics. Be careless once and all the safety is gone, and then it will be harder to debug than plain old C.
Exceptions are a specific solution to a specific problem. E.g. you're an OS an you're pushing out a minor-version maintenance update which tightens security policy, adding new failure codes. You cannot expect every user application to be recompiled. Exceptions ensure that a loosely-coupled interruption channel exists in user code by compiler-fiat. Other uses are specious, but let's not throw the baby out with the bathwater.
I use C++ everyday and I would describe my usage as mostly "C with templates and destructors." I also tend to avoid OOP paradigms if possible.
All the modern C++11 and above features are great if you're using standard lib and algorithms. But I really don't want to implement copy/move semantics, iterators etc for my own classes. Modern C++ feels more python-esque with a lot of syntax sugar (which is great for expressiveness).
I have the same experience from implementing an interpreter [0] in C++ and later rewriting from scratch in straight C. The popular advice to use C with classes and skip the rest doesn't hold, for me it always turns into a constant loosing struggle to keep the code clean of fancy abstractions. And like the author mentioned, fixing performance problems suddenly means replacing STL features, which is a lot more work than simply writing what you need. Assuming you can figure out which knob needs turning in the first place, that is.
I first came across Turbo C++ back in 1995; it was the third language I learned, several years before I started my C journey. But I've gradually come to the point where I consider C++ to be a bad compromise, period. For low level fundamentals, C works better; and for higher level code there are plenty of more convenient options.
Those solutions have other consequences because of how everything interacts. No exceptions means no std and you have to be very careful about what other libraries might do. Likewise it makes the consuming code more complicated, they now have to deal with return codes and exceptions.
You can't really just avoid language features like that.
Many large C++ projects--LLVM, Chromium, Firefox--disable C++ exception handling. They still use the standard C++ library.
Exceptions in the standard library boil down to a) passing through user's exceptions, b) failing memory allocation (note that some systems, such as Linux, won't fail a memory allocation but instead crash you when you try to use it), and c) throwing exceptions when you exceed the bounds of an array or the like (~= Java's RuntimeException). With exception handling turned off, class (a) doesn't exist and classes (b) and (c) turn into hard program crashes, which is almost invariably what you want to do for these errors anyways.
Despite the fact that C++ has been my main programming language for a decade, I don't actually know how to write catch clauses correctly in C++, unlike Java, Python, and JS. That's how easy it is to avoid C++ exceptions.
> some systems, such as Linux, won't fail a memory allocation but instead crash you when you try to use it
False.
1) You can disable overcommit, and there are many of us that do this as a matter of course on all our Linux servers.
2) malloc can fail because of process limits (e.g. setrlimit or cgroups).
I don't program in C++, but I do use RAII-like patterns in C. By that I mean that when I create and initialize objects, all the necessary internal resources--particularly those that rely on dynamic allocation--are also created and initialized in the same routine.
That means most places where memory allocation can fail are grouped closely together into a handful of initialization routines, and the places where allocation failure results in unwinding of a logical task are even fewer. (While C doesn't automatically release resources, following an RAII-like pattern means deallocations are just as structured and orderly as allocations.)
I can understand bailing on allocation failure in scripting languages--not only is there much more dynamic allocation taking place, but allocation happens piece-meal all over the program, and often in a very unstructured manner (strings of variably length generated all over the place). Furthermore, often script execution occurs in a context that that can be isolated and therefore unwound at that interface boundary--i.e. a C- or C++-based service executing a script to handle a transaction.
But in languages like C++ and Rust, especially for infrastructure software and libraries, it's a sin IMO. These are languages intended for use in situations where you can carefully structure your code, they make it trivial to minimize the number of allocations (because POD-oriented), and they permit one to group and isolate allocations in ways (e.g. RAII) that make it practical (if not trivial) to unwind and recover program state.[1]
But why even bother?
1) Because these languages are often used in situations where failure matters. A core piece of system software that fails on malloc is a core piece of system software that is unreliable, and programs that rely on you can behave in unpredictable and even insecure ways.
1.a) Go authors take the view that reliability comes from running multiple instances in the cloud. Yes, that's one way, but it's not the only way, not always an option, and in any event anybody with enough experience dealing with "glitches" in cloud services understands that at least in terms of QoS there's no substitute for well-written, reliable service instances.
1.b) Security. It's often trivial to create memory pressure on a box. OOM killers are notorious for killing random processes, and even without overcommit the order of allocations across all processes is non-deterministic. Therefore, not handling OOM gives attackers a way to selectively kill critical services on a box. Disruption of core services can tickle bugs across the system.
2) Overcommit is an evil all its own. It leads to the equivalent of buffer bloat. Overcommit makes it difficult if not impossible to respond to memory resource backpressure. This leads to reliance on magic numbers and hand-tweaking various sorts of internal limits of programs. We've come full circle to the 1980s where enterprise software no longer scales automatically (which for a brief period in the late 1990s early 2000s was a real goal, often achieved), but instead requires lots of knob turning to become minimally reliable. Nobody questions this anymore. (Ironically, Linux helped lead the way to knob-free enterprise operating systems by making kernel data structures like process tables and file descriptor tables fully dynamic rather than statically sized at compile or boot time, so the kernel automatically scaled from PCs to huge servers without requiring a sysadmin to tweak this stuff. Notably, Linux doesn't just crash if it can't allocate a new file descriptor entry, nor do most programs immediately crash when open fails. Even more ironically, overcommit on Linux was originally justified to support old software which preallocated huge buffers; but such software was written in an era where sysadmins were expected to tailor hardware and system resources to the software application. Overcommit has had perpetuated the original sin.)
Not all software needs to handle OOM. Not event all C, C++, or Rust components. But for infrastructure software OOM should be handled no different than file or network errors--subcomponents should be capable of maintaining consistent state and bubbling the error upward to let the larger application make the decision. And if you're not writing critical infrastructure software, why are you using these languages anyhow?[2] If a language or framework doesn't permit components to do this, then they're fundamentally flawed, at least in so far as they're used (directly or indirectly) for critical services. You wouldn't expect the Linux kernel to panic on OOM (although some poorly written parts will, causing no end up headaches). You wouldn't expect libc to panic on OOM. There's no categorical dividing line beyond which developers are excused from caring about such issues.
[1] Granted, Rust is a little more difficult as it's hostile to allocation-free linked-lists and trees, such as in BSD <sys/queue.h> and <sys/tree.h>. Hash tables require [potential] insertion-time allocation. Still, it's not insurmountable. Many forms of dynamic allocation that can't be rolled into a task-specific initialization phase, such as buffers and caches, are colocated with other operations, like file operations, which already must handle spurious runtime failures, and so the failure paths can be shared.
[2] Maybe for performance? Performance critical tasks are easily farmed out to libraries, libraries are particularly suited to handling OOM gracefully by unwinding state up to the interface boundary.
Linux, in most distributions, enables overcommit. That is a fact that anyone distributing software is going to have to deal with. Saying that you personally choose to disable it whenever possible doesn't make that fact go away.
> But for infrastructure software OOM should be handled no different than file or network errors--subcomponents should be capable of maintaining consistent state and bubbling the error upward to let the larger application make the decision.
OOM, to me, is more like a null pointer dereference or a division by zero. If it happens, it's because I as a programmer screwed up, either by leaking memory, having a data structure that needs to be disk-based instead of memory-based, or by failing to consider resource bounds.
The problem with trying to handle memory allocation is that a) it can happen just about anywhere, b) you have to handle it without allocating any more memory, and c) if there is a single place where you forget to handle allocation failure, your program is not robust to OOM errors. I rather expect that the fraction of programs that could not be crashed with a malicious allocator that returns allocation failure at the worst possible time is well south of 0.01%.
That's like saying a NULL pointer dereference or division by zero can happen anywhere. Only for poorly written code that doesn't think through and maintain its invariants. Languages like C++ and Rust make it easier to check your invariants, but plenty of C code does this, including kernels and complex libraries. And they don't do it by inserting assertions before every pointer deference or division operation.
As I said, the RAII-pattern is one way to keep your invariants such that most allocations only happen at resource initialization points, not at every point in a program where the object is manipulated.
> b) you have to handle it without allocating any more memory,
Unwinding state doesn't need more memory, not in languages like C, C++, or Rust. If it did then the kernel would panic when cleaning up after programs terminated when out of memory.
This argument is a common refrain from GUI programmers who wish to throw up a notification window. But that's distinct from recovering to a steady state.
In those particular contexts where recovery isn't possible or practical, then you can't recover. Again, scripting languages are an obvious example, though some, like Lua, handle OOM and let you recover at the C API boundary. (Notably, in Lua's case the Lua VM context remains valid and consistent, and in fact you can handle OOM gracefully purely within Lua, but only at points where the VM is designed to be a recovery point, such as at pcall or coroutine.resume.) But the existence of such contexts doesn't mean recovery is never possible or even rarely possible.
> c) if there is a single place where you forget to handle allocation failure, your program is not robust to OOM errors.
If you keep an RAII pattern then handling OOM is little different than deallocating objects, period. In that case, your statement is the equivalent of saying that because memory bugs exist, no program should bother deallocating memory at all.
Now, I've seen programs that can't handle deallocation; programs that were written to be invoked as one-shot command-line utilities and never concerned themselves with memory leaks. Trying to fix them after the fact so they can run from a service process is indeed usually a hopeless endeavor. Likewise, trying to fix a program that didn't concern itself with OOM is also a hopeless endeavor. But it doesn't follow that therefore when starting a project from scratch one shouldn't bother with OOM at all, no more than saying nobody should bother with deallocation.
The objections people have to OOM handling are self-fulfilling. When programmers don't consider or outright reject OOM handling then ofcourse their code will be littered with logic that implicitly or explicitly relies on the invariant of malloc never failing. So what? You can't infer anything from that other than that programs only work well, if at all, when the implicit or explicit assumptions they were written under continue to hold.
I've been writing C++ for ten years and I can count the number of try/catch blocks I've written on one hand. The only exception I occasionally want to catch is std::bad_alloc. Other exceptions are basically the equivalent of an assert or a segfault. They will never be thrown unless there is an error in my program.
> You can't really just avoid language features like that.
As jcranmer points out, forgoing C++ exceptions is literally written policy for the browser you are likely reading this with right now. And no, you don't give up std.
C++ exceptions have been a misfeature since inception.
- Error flag in the class (not a good solution, though, as others have noted)
- Factory function that returns a smart pointer, or null on error.
- Just ensure your constructor never fails. (This probably means aborting on allocation failure. In practice that’s usually fine, depending on application.)
This is how you get the equivalent of `if err != nil { return err; }` everywhere in your codebase. We've already come to the conclusion that this pattern is terrible.
What you really need is to separate object allocation from construction. But then you're adopting an object model that C++ just doesn't support.
It sucks versus Foo() { if(...) throw whatever; }.
It needs more code (check for constructed() every time, and if you forget to checking it you've introduced a bug), and is less performant (since you will have one additional branch in the "if(constructed())" mandatory check.
The ‘error’ member variable would take an additional space in the object that otherwise could be as small as a byte. C++ is carefully designed to avoid unnecessary overhead.
I never understand this kind of reasoning. Sure, C++ has exceptions, but you are not forced to use them. Just because you go to a restaurant and they have dessert on the menu it doesn't mean you have an obligation to order it.
In fact, you can easily define a result type templated by your result/error code (similarly to Rust or OCaml/Base) and use it in your whole codebase.
I never use virtual or exceptions in my C++ code, for instance, and I rarely need anything fancy like smart pointers.
Maybe one could argue that the language is too complex and supports too different programming styles, but that doesn't justify when the programmer chooses the worst way to do it: that's on the programmer and not on the language.
Can someone please suggest book or exercises to understand exceptions? I was working on Java project and I was not productive because I could not understand stand where I should throw errors and which function to catch it. I did not get far and was eventually removed from the project because of the buggy code. I think it happened because i don't understand how to write code using exceptions. What layer of code should i catch exceptions? How to deal nested function calls?
Errors are the only way the Programmer can tell anybody else what is happening (when it is outside the norm). They are, in a way, more important than the usual results.
On the other side, Error handling is the only way The Programmer can acknowledge what above has said (and do something about it).
And while the software itself is just a way of communication, the above conversation rarely exists in it. Most times it isnt mentioned even in high-level pre-software "software", that is, specs.
For me, the most infuriating part of C++ is the error reporting. Even for trivial programs, I have to do make 2>&1 |less and then /error just to get to the first of 15 errors mixed in with 35 warnings that span 3 pages, all from one typo.
C++ has come a long way since the 90s, but it's still an incredibly clunky and obtuse language, with 10x more ways to do it wrong than right.
If you use an IDE you just click on the lines in your error log which brings you to where the problem is. Though nowadays IDEs such as QtCreator use clang to analyse your code in real-time and so you see the errors appear as in-line hints as you type.
Given that C++ is a superset of C I don’t get why the author wouldn’t just cherry pick the features he needs. Nobody is forcing you to use exceptions or the STL in C++ but you can still get templates and namespaces and smart pointers and references and const and better string handling...
C++ is definitely not a subset and its not just from purely linguistic considerations.
Also, given that zeromq is a community-driven project, requiring the contributors to use C++ but avoid 50% of features is weird and non-idiomatic at best.
If you want to C -- C.
"The decoupling between raising of the exception and handling it, that makes avoiding failures so easy in C++."
The point of exceptions, over C-style errors codes, is that you cant forget to handle errors.
"When you create an instance of the class, constructor is called (which cannot fail) and then you explicitly call init function (which can fail)."
That's an anti-pattern. Initialization should happen in the constructor, not be separated into another step. (Look up RAII)
If you cant make exceptions work for you (for instance if you are making drivers or embedded programs), I have good news for you. Lightweight exceptions are being worked on and will most likely end up in the standard in 2023.
"It's far more readable and — as a bonus — compiler is likely to produce more efficient code."
I want to learn more about this, when or how do you know a compiler produced efficient code? Does anybody have any interesting links for reads on this?
You profile the generated code, be that in terms of execution speed or size in bytes: If it's better then it's better.
In this case C++ exceptions are practically zero cost so I don't quite believe that the compiler will generate more efficient code unless it elides some of the C version based on context (Otherwise the if statements aren't free)
Well, I think most people do it by some sort of "trial and error", the tool of choise is Matt Godbolt's Compiler Explorer [1]
I sometimes listen to the CppCast Podcast and one of the Hosts, Jason Turner, likes to write code in modern C++ for some vintage computers. He tests code bits in Compiler Explorer for that. He also has a YouTube Channel where he does that [2]
The C++ code can be compiled in such a way that it has no branching e.g. stack unwinding instead, which is slower in the case of an exception being thrown - However, exceptions shouldn't be used for non-exceptional circumstances.
Someone smarter than me once pointed out that exceptions are great as long as you don't have to worry about rolling back state. So, they are a great match for pure functional, stateless languages. But, the more procedural the language, the more complicated exceptions become. C++ can be very procedural and I'm of the opinion that I'm not smart enough to use exceptions in C++. Instead, I'm playing with Expected and monadic error handling.
> Someone smarter than me once pointed out that exceptions are great as long as you don't have to worry about rolling back state
and the point of exceptions in C++ is that they have to be used in combination with the "rolling-back state" feature backed in the language, namely RAII. It's only a pain in language without such things such as C# / Java, but in C++ if I do
file my_file;
// .. do stuff
auto f = std::make_unique<whatever>(...);
// ...
throw some_error;
I know that my_file will be closed if it had been opened, and that my memory will be freed.
Of course if your code is littered with `new` and `fopen` left and right that won't work, but the code won't pass code review in the first place :-)
If avoiding undefined behavior at all costs is important, I'm not sure C is the right choice either. Rust would also avoid exceptions and eliminate most (all?) of the undefined behavior that C has.
Well maintained C++ codebases have a style guide limiting programmers to a restricted subset of the language. Not allowing exceptions at all is often a good idea.
Rust was still years away from 1.0 when this was written. If we consider Rust as it is today, it would address some of the author’s complaints about C++, but not all. Rust’s error handling is more explicit and supports fallible “constructors” (Rust doesn’t actually have a true concept of a constructor) – but it doesn’t support fallible destructors, and the standard library notably lacks the ability to cleanly handle out-of-memory conditions. Privacy is more flexible, and there’s not as much of a strict “object-oriented” focus. On the other hand, intrusive lists are arguably even worse than in C++, because they require a lot of unsafe code and don’t play well with the borrow checker (in particular, you can’t enforce unique access).
What the article attempts to exemplify is conciseness and transparency. C++ is inherently OOP and OOP is not concise or transparent.
For example when you extend an object the object knows of its chain of inheritance at compile time, but this is not immediately evident from reading an extended instance in the code. This allows code that is simple to write and expand, but more challenging to maintain. It also results in a large amount of boilerplate programming by convention.
The way I prefer to think about this is programming as a means of communication. There are three mental models of communication: explicitness, implicitness, and stupid.
Inherited object instances are implicit structures. You have some idea of what they are because of where they come from and what you know about that thing they come from. This requires some amount of reasoning that isn't directly spelled out. Implicitness is a convenience that scales well. In spoken language the most common implicit things are pronouns.
Creating everything as an explicit quality takes a tremendous amount of work. The result though is clarity in that you know what you are looking at by simply reading it. Counter-intuitively explicit things are not necessarily more code and in many cases actually result in less code. This is the nature of sacrificing convenience for precision.
in 1985 maybe. It was already pretty much not OOP in 1999 with libraries such as boost.graph, and is as far from "Java/C#" oop that you can imagine in 2018, where you can easily build lazy functional pipelines (https://github.com/ericniebler/range-v3), pattern-match (https://github.com/mpark/patterns), and where generic programming is the dominant paradigm.
On the contrary, I believe we should expect to spend the majority of software engineering time writing and thinking through error-handling code, before even your first test[1].
I'd even go so far as to say: If you're not spending at least half your time on so-called 'error handling', you're probably doing something wrong, like using a language feature (like exceptions) to defer that technical debt to later -- and you'll regret it, if your project matures, I assure you.
This is why I so greatly appreciate languages like Rust and Zig[2] which remove exceptions and unchecked null pointers from the language entirely (with few edge-case exceptions), and provide a powerful and robust type system that allows us to express and handle error conditions naturally, safely, and elegantly.
[1] To be clear, by no means am I downplaying the importance of test code, or even manual testing; rather, I'm arguing that purely "test driven development" is not sufficient to yield extremely robust software of significant sophistication.
[2] These aren't the only examples, but they're among the only that aim to be C++ (Rust) and C (Zig) replacements, that also made the "right" design choices (IMO) in removing both exceptions and unchecked null references.