It leads to the current situation where you have C++ "the language" which is everything, and then C++ "the subset that everyone uses" where that subset constantly changes with time and development context.
But yea this kind of thing hit me recently on the interview circuit. I wrote some correct C++ (in that it was correct for the idioms in vogue when I last wrote C++ regularly for money) but I got feedback I wasn't as senior as they hoped due to my (lack of) C++ knowledge. Part of that was a shitty interviewer, but it's also just a fundamental part of the language. If you leave for a few years or try and change shops you find that everything under you has been removed or a completely different subset of the language is being used elsewhere. The complete lack of an ecosystem just reinforces that.
It doesn't happen only in C++ circles.
While C++'s churn frequency seems to be higher than the Javascript front end churn frequency, as an outsider, it still seems like "best practices" on C++ are churning around 1.5-2 years, it's been happening for my entire career, and it's still happening. If I seem a bit unsympathetic to the claims that the problems are solved if you just write your C++ code this way now, it's because I first heard that in 1998 or so, for a set of common practices now considered laughably out of date, of course.
At some point it becomes more cost-effective to just "churn" on to Rust next time, because even though in that same time frame Rust is a younger language that was going through its early design iteration phase it still seems like it has settled into a lower-frequency churn rate lately for "best practices" than C++.
There's probably some interesting and deeply profound reason why C++ just can't seem to stabilize across what is approaching an entire human generation, but I'm nowhere near interested enough in learning it to actually learn the amount of C++ it would take to find it.
Rust is still adding a ton of new language features, especially around async, compile time code evaluation and the type system (const generics, GAT/HKT, existential types etc.). We'll very likely see further developments in more areas next, e.g. to match C++ developments in parallel and heterogenous compute (GPU's and the like), or to add forms of proof-carrying code, etc.
C++ is constantly changing what a "best practice" is. The latest hotness from three-generations-ago churn is now considered broken.
Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
Being responsive to the needs of its community of programmers is job one for a language committee. If new, better ways don't arise, your language has stagnated.
You're right it's not accidental, but the problem is that when you continually add new features you end up creating a confusing mess. Yes, the new features may respond to some need in the community, but by adding it you've also:
- Introduced possibly unforeseen issues because features are never added in isolation; they interact with one another, and the more features you have the harder it is to test them all.
- Created confusion because now all the old information on the internet is out of date
- Everyone needs to update their tooling to support the new features.
- Make it harder for new people to start learning the language
> Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
This is a nice idea but it doesn't reflect reality. If the C++ user survey [0] is to be believed, a full 67% of users were restricted from using the latest C++ at the time (either fully or certain features).
> Being responsive to the needs of its community of programmers is job one for a language committee.
This is true but it also must be balanced against mission scope creep and design considerations. Being everything to everyone is not design.
[0] https://isocpp.org/files/papers/CppDevSurvey-2018-02-summary...
People not using the latest Standard are waiting for support within their particular environment, not because they want to stay with a less performant version of the language.
> Failing to use the new feature is just failing to write code the best way that is available right now. In 2013 you had no choice but to do it the old way; but you don't have to anymore, because the language and std library have caught up with you.
What I hear you saying is that failing to use new features is a failure to write the best code available, and that before maybe you didn't have a choice but not anymore.
And yet, huge chunk of C++ devs are forbidden by their organizations from using various C++ features that have been added throughout the years. It's not just C++ 17 but it goes back even further. This is the whole point of TFA. So it's not just that people are "waiting" for tools to catch up, unless you have evidence of this.
When an organizational logjam is broken, programmers can immediately switch over to the better, more modern way of coding enabled by the newer Standard they are allowed to use. If they were on C++98, and now they can use C++14, they are still better off than before, even if the current Standard is C++20: they will be able to code according to best practices for C++14, which are better than for C++98.
C++20 is adding a module system to C++. That's a major change to the compilation model. It is also adding concepts, a major change in the way templates are to be written. The very article we're commenting on is discussing how exceptions should possibly be replaced by another error report mechanism, several proposals are in flight for this. There's also the "destructive moves" proposals that have the potential to change a lot how we write types.
A telltale sign that these changes are major is that the entire std has to be "modularized" to support modules, and to be modified to support concepts. Similarly if exceptions are to be revised a large chunk of exception using functions from the std would need to be modified.
On the Rust side, I think the only change that has even a comparable impact is const generics (and maybe specialization).
Existential types and GAT will change how to express some traits (allowing a LendingIterator for example), but I don't expect they will affect a large portion of Rust's std.
Also of note is that the Rust changes come to add new systems orthogonal to the existing features (const generics fill an obvious void compared to C++, same with GAT and existential types where Rust's expressivity is limited in comparison with C++ atm). By contrast in C++, the module system comes to replace the headers, and a change to exceptions would replace the current exception system, creating churn.
Indeed, instead of crazy tricks with SFINAE and tag dispatch, templates can be much easily written.
I have to disagree with this quite strongly. What "best practices" churn are you seeing? Your reference example of "don't use new & delete" (which I agree with) was a single best practice change that happened ~10 years ago. Similarly https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines is like 5-7 years old now, and afaik hasn't had any significant revisions?
The "churn" was really pre-C++11 to post-C++11. It was more like a python 3 moment than churn, other than it's taking a long, long time for code bases to catch up.
It's not like C++ is oscillating. It continues to improve, which is a good thing.
"I will contend that conceptual integrity is the most important consideration in system design. It is better to have a system omit certain anomalous features and improvements, but to reflect one set of design ideas, than to have one that contains many good but independent and uncoordinated ideas."
IMO C++ does not have conceptual integrity.Your complaint is that it is not dead.
Matlab is not the same language it was in the 70s. But the core language is still largely about manipulating arrays and it does it well. It has evolved by adding functionality through toolboxes and creating new, innovative developer tooling. But it hasn't jumped on every PL bandwagon that has driven by over the last 50 years.
Matlab's usage is down a bit since a peak at 2017, but over the last 20 years it's up over 300%, and it's done so by being a for-profit language. Mathworks is a billion dollar company, which is quite an achievement in the PL space in 2022.
Meanwhile C++ usage is up over the last couple years but the long term trend has been a steady decline over the last 20 years [1]. From 14% to a low of 4%, now back up to around 8%.
TIOBE is statistical noise. You would equally meaningfully cite your tea leaves, or crows flying overhead.
I mean, if TIOBE was really statistical noise as your claim, there wouldn't be clear trends in industry reflected in the data, like the rise of Python in the last few years. And yet we see it, so clearly it's measuring something.
All languages that are actually useful evolve. They get features that other people need, and you don't, just yet. Some of those features do turn out not to be perfect, because they are made by humans.
Comparing an old language to a new language, the new language will have many fewer of those both because it leaves many old things behind, and because it gets benefit of hindsight for the rest. Give it time, and it will accumulate "incoherence" of its own; the faster it evolves, the faster that happens.
The alternative is for a language not to be used. Then, it can stay pristine and "coherent", and not useful.
This is the human condition.
No, I actually teach C++ and have been keeping up with it for decades. It was the second language I learned in 1994 and I still code in it professionally today.
> Give it time, and it will accumulate "incoherence" of its own; the faster it evolves, the faster that happens.
Like I pointed out with Matlab, it’s a much older language compared to c++ yet is mostly coherent, far more than c++. This has less to do about C++’s age and the March of time or the human condition. Otherwise more old languages would be as incoherent as c++ yet that’s not the case.
My students give me high marks, my department (which includes faculty who have contributed to C++ spec over the years) is satisfied with my teaching, and I graduate students that go on to work at top companies and research labs around the world. I'm doing my job just fine, let's stick to talking about C++.
Right now, to learn C++ in a generic way, you need to learn pretty much all of programming paradigms and all of their variations - which is not a thing I would consider a plus.
I disagree, I would recommend learning enough where the tool starts providing value to YOU for YOUR problem domain and then pause/resume as needed. The main purpose of any programming language is to be productive in it and use it to solve a problem. In my opinion, there is no reason to learn more than you need about C++ unless you were a compiler author, on the C++ standards committee, or something similar.
There are great uses and great features, but there are so many of them and everyone has their own opinions.... Even in this thread there's a clear subset of people who "adore" C++ exceptions
It is not the job of a general-purpose language to insert its own opinions. That is the system designer's job. Fighting with the language's opinions is a recipe for failure.
Whereas when in an opinionated language and you cannot do what you want... Ugly hacks such as people using bash scripts, mustache templates, etc ... to preprocess their code to get what they want quickly happen.
(Perhaps the clearest C++ opinion is "zero cost abstractions")
- zero cost abstraction
- system language
- compatibility with C toolchain
And I would have to say the average C++ dev did not know about RAII in 1993.
But what exactly is wrong with that? I don't quite understand your argument here..
Prolly not, I expect the issue comes from the increase in branches since a value-based error reporting has to branch on every function return. Even if the branch is predictible, it’s not free.
And fib() would be a worst-case scenario as it does very little per-call, the constant per-call overhead would be rather major.
The intended use-case of catch_unwind is to protect the wider program e.g. avoid breaking a threadpool worker or a scheduler on panic, or transmit the information cross threads or to collation tools like sentry.
A problem with using exceptions instead is they defeat the return address prediction by unwinding the stack.
Anyway the hint we really need, no ISA has: "do not predict this branch". (We approximate that with constructs that generate a "cmov" instruction, which anyway is not going away.)
How does using exceptions defeat return address prediction? You are explicitly not returning, so any prediction would be wrong anyway. In the common case, you do return, and the predictor works fine.
It was the same size on PPC, and on x86 using recommended branch directions (but not prefixes).
> Compilers nowadays use code motion to implement branch hinting, which does not burn L1 cache. (Maybe code motion is what you mean by "hot/cold splitting"?)
Hot/cold splitting is not just sinking unlikely basic blocks, it's when you move them to the end of the program entirely.
That doesn't hint branches anymore, though; Intel hasn't recommended any particular branch layout since 2006.
> How does using exceptions defeat return address prediction? You are explicitly not returning, so any prediction would be wrong anyway.
Anything that never returns is a mispredict there; most things return. What it does instead (read the DWARF tables, find the catch block, indirect jump) is harder to predict too since it has a lot of dependent memory reads.
Machines do still charge an extra cycle for branches taken vs. not, so it matters whether you expect to take it.
Negligibly few things never return; most of those abort. Performance of those absolutely does not matter.
Why should anyone care about predicting the catch block a throw will land in after all the right destructor calls have finished? We have already established that throwing costs multiple L3 cache misses, if not actual page faults.
It's not about the benefit, that's just the easiest way to implement it - put it in a different TEXT section and let the linker move it.
Although, there is a popular desktop ARM CPU with 16KB pages.
> Machines do still charge an extra cycle for branches taken vs. not, so it matters whether you expect to take it.
Current generation CPUs can issue one taken branch or 2 not-taken branches in ~1 cycle (although strangely Zen2 couldn't), but yes it is better to be not taken iff not mispredicted. (https://www.agner.org/optimize/instruction_tables.pdf)
> Negligibly few things never return; most of those abort. Performance of those absolutely does not matter.
Throwing an exception isn't a return, nor longjmp/green threads/whatever. Sometimes they're called abnormal or non-local returns, but according to your C++ compiler your throwing function can be `noreturn`.
Error path performance is important since there are situations like network I/O where errors aren't at all unexpected. If you're writing a program you can just special case your hotter error paths, but if you're designing the language/OS/CPU under it then you have to make harder decisions.
> Why should anyone care about predicting the catch block a throw will land in after all the right destructor calls have finished? We have already established that throwing costs multiple L3 cache misses, if not actual page faults.
More prediction is always better. The earlier you can issue a cache miss the earlier you get it back.
For instance, that popular desktop ARM CPU can issue 600+ instructions at once (according to Anandtech). That's a lot of unnecessary stalls if you mispredict.
And so its vendor has their own language, presumably compatible with it, which doesn't support exceptions.
Specifically, a prediction is not better when it makes no difference. Then, it is worse, because it consumes a resource that would better be applied in a different place where it could make a difference.
Exceptions are the perfect example of a case where any prediction expenditure would be wasted; except predicting that no exception will be thrown. It is always better to predict no exception is thrown, because only the non-throwing case can benefit.
Running ahead and pre-computing results that would be thrown away if an exception is thrown is a pure win: With no exception, you are far ahead; with an exception, there is no useful work to be sped up, it is all just overhead anyway.
This is similar to a busy-wait: you are better off to have leaving the wait predicted, because that reduces your response latency, even though history predicts that you will continue waiting. This is why there is now a special busy-wait instruction that does not consume a branch prediction slot. (It also consumes no power, because it just sleeps until the cache line being watched shows an update.)
For the record, I like absl::StatusOr<>.
As the article explains, the problems in C++ are more an ABI issue than a programming language issue (except for the by-reference vs. by-value semantics). You could implement exceptions internally by variant-like return values, for example, similar to how error passing is done in Rust, while still having it look like exceptions on the language level. It would be fun for future languages and runtimes to more easily be able to switch the underlying mechanism, or possibly to be able to use different implementation mechanisms for different parts of a program as needed.
Especially when used as an error ADT they're awful because they mix everything from "you have to handle this 90% of the time" to "the universe physics just changed, sorry" into one construct. Much better to use something like Vavr's Try and explicitly propagate the error as a value.
[1] https://www.artima.com/articles/the-trouble-with-checked-exc...
If I declare "throws NullPointerException", then this should mean I want it to be a checked exception. This should force the caller to catch the exception, or declare throwing it, or to simply throw it out again without declaring it. This would effectively convert the checked exception into an unchecked exception.
Converting a checked exception to an unchecked exception is possible in Java, and I do it whenever it makes the most sense instead of wrapping the exception, which is just a mess. Unfortunately, there's no way to make an unchecked exception behave as if it was a checked exception. It's easier to reduce fault tolerant checks then to add more in.
Some might argue that converting an exception to be unchecked is a bad idea, but this sort of thing is done all the time in JVM languages that are designed to interoperate with Java. If I call a Scala library from Java, and that library is doing I/O, then IOException has now effectively become unchecked.
I understand your point about usage-dependend checking. However, I believe it is mistaken. Consider a call chain A -> B -> C -> D, where D throws a checked exception of type X, which C converts into an unchecked exception (still type X). At the same time, B also calls other methods that happen to throw a checked X, and thus B throws a checked X itself, which now, unbeknownst to B, also includes the X from D. B documents the semantic of its X exception, which may not fit the ones thrown by D. Now A catches X, believing the semantics as documented by B, but actually also catches X from D, which was concealed (made unchecked) by C. This breaks checked exceptions in their role as part of an interface contract.
The fact that unchecked exception may originate from arbitrarily deep in the call chain is also the reason why they are unsuitable for defining interface contracts, A function declaring certain semantics for a particular unchecked exception can't realistically ensure those semantics if any function it calls itself must be assumed to also throw exceptions of that type (because it is unchecked). Effectively, you can't safely document "exception X means condition Y" for your function if any nested calls may also throw X for unknown reasons.
At the point where it is evident the program has a bug, there is nothing known, anymore, about the state of the program. It means that data structures defining the state are inconsistent to a wholly unknown degree, and any further computation depending on or,worse, altering that state can do no better than compound the failure.
I always delete code I find that tries to recover from a bug. It is impossible to recover from a bug; the best that can be done is to bail out as fast as possible.
My example of declaring an NPE as checked isn't something I would actually encourage. It conveys very little context, and exceptions which extend RE should signal a bug in the code. I should have selected a better example.
If you have a function that has an abnormal result as its API, it should have that as part of its return value, because returning as values is what you do with results. Checked exceptions in contrast don't compose. See for example this code:
X myFunction(Y param) throws MyFailureException();
List<X> transformed = list.stream()
.map(e -> myFunction(e))
.collect(toList());
This is not allowed, because a the lambda in map can't throw. Even if that was allowed map would need a generic "throws Exception" as its API, which would be awful. With checked exceptions the only possibilities you have is catch the exception locally at the point of calling the function or stop as soon as you encounter the failure and bubble it up.Instead, if you make the part of the return value you can do whatever. I'll use Vavr Try as an example, but you can also do this in Java 17 with sealed interfaces or in a miriad other ways.
Try<X> myFunction(Y param);
List<Try<X>> transformed = list.stream()
.map(e -> myFunction(e))
.collect(toList());
Now you can still handle failure locally, but you can also check the whole list and make a decision based on that. Or you can propagate the results including failures because this is not the best place to handle them. That's what I mean with composes vs not composes.That is, the checked part is just a language implementation, right? The jvm doesn't really make much of a distinction. (This is meant as a check to my assumption.)
Regardless, I'd be interested in seeing if this is a performance bottleneck. I'd guess it is only relevant on dataset processing. Closer you are to a place that legitimately can toss to a user, more likely you are to not care?
That is, if the common case of an exception is to stop and ask for intervention, is this a concern at all?
You can when you implement your own exception type, but not in general (and doing so would break too many things).
Exceptions are thrown and caught quite frequently in Java for "expected" cases, for example when attempting to parse a number from a string which is not a valid number. It's generally not a performance problem, and stack trace collection is probably heavily optimized in the JVM. Nevertheless, it's certainly still a lot slower than C++ single-threaded exceptions. You have to realize that even a factor of 100 slower may be unnoticeable for many use cases, because so much else is going on in the application.
But, yes, I realize most of these are probably not noticable.
As always someone is wrong on Internet.
Java adopted a feature that was initially introduced in CLU, adopted by Mesa/Cedar, Modula-2+ and Modula-3, was being considered for ongoing ISO C++ standardization at the time.
By people who do not want to believe that errors are part of a system's API and prefer to just write the happy path and let any exception kill the process. And who don't mind getting called at 2:00 am because a dependency buried deep in a subsystem threw an exception that you'd never heard of before.
This is a really interesting and nuanced point here. The article linked above [1] talks a bit about it. The problem is, they both are and aren't part of the API in the strict sense.
In the sense that they specify a contractual behaviour they are part of the API of a function. But in the sense that they are something the caller should / needs to specifically care about, they sit in between. That is, in the vast majority of cases, the caller does not care specifically what exception occurred. Generally they want to clean up resources and pass the error up the chain. It is "exceptional" that a caller will react in a specific way to to a specific type of exception. So this is where Java goes wrong because it forces the fine grained exception handling into the client when the majority case (and preferred case generally) is the opposite. It makes you treat the minority case as the main case. There are ways to work around / deal with this but nearly all of them are bad. The article talks about some of the badness.
I do think it's interesting though that Rust has taken off and is generally admired for a very similar type of feature (compiler enforced memory safety). I am really curious how that will age, but so far it seems like it is holding up.
[1] https://www.artima.com/articles/the-trouble-with-checked-exc...
Case in point, the many checked exceptions in the Java standard library that are never, ever thrown. Ever. Such as ByteArrayOutputStream#close(). In fact the docs on that method even say it doesn't do anything, and won't throw an exception. But there's still a checked exception that you have to handle, because it came from the interface.
Which is part of why checked exceptions are a mistake. If Java wasn't so aggressively OOP, then maybe there's a good idea there. Like maybe checked exceptions in C++ would work, as you're not relying (as much) on inheritance to provide common functionality. But as soon as interfaces & anonymous types (eg, lambdas) enter the scene, it starts falling over.
Also in terms of API design, checked exceptions are quite limiting. Especially when working with asynchronous APIs, as checked exceptions are inherently coupled to synchronous calling conventions.
And there's also then the problem of not a whole lot of your code base is an "API surface" (hopefully anyway), and it's really vague how a middle layer should handle checked exceptions. Just propagate everything? But that's a maintenance disaster & leaks all sorts of implementation details. Just convert everything to a single type? Well now you can't catch specific errors as easily.
The point was that you should be returning errors, not throwing them. Runtime exceptions (null reference, division by zero, out of memory, etc.) ought to indicate a fatal error in the (sub)program or runtime environment. You can trap these, and report them, but it's usually a mistake to try to case-match on them. Unlike errors, which are predictable, enumerable elements of the design, runtime exceptions should be treated as an open set.
That is, if the problem is likely one that needs operator/user intervention, the non local semantics of exceptions makes a ton of sense. Indeed, it is useful to have a central handler of "things went wrong" in ways that is cumbersome if every place is responsible for that.
The condition system in Common Lisp (which I am also a fan of BTW) is designed around dealing with conditions when they occur, whereas most of the alternatives focus on the aftermath. In particular, conditions don't unwind the stack before running their handlers, which makes it possible to correct the issue and continue, though handlers can naturally choose to perform non-local returns instead. More to the point, there is no requirement to annotate Common Lisp functions with the conditions they may raise, which makes them more akin to unchecked exceptions.
I view this as I want my engine to mostly just work. It may need to indicate "check engine" sometimes, though. And that, by necessity, has to be a side channel?
I think that is my ultimate dream. I want functions to have a side channel to the user/operator that is not necessarily in the main flow path. At large, I lean on metrics for this. But sometimes there are options. How do you put those options in, without being a burden for the main case where they are not relevant?
That is the essence of the Common Lisp condition system, and you can get there in most languages with closures, or at least function pointers, and exceptions or some other non-local return mechanism using a combination of callback functions for the conditions and unchecked exceptions for the default, unhandled case. The key is that you don't try to catch the exceptions, except at the top level where they are simply reported to the user. Instead you register your condition handler as a callback function so that it will be invoked to resolve the issue without unwinding the stack. It helps to have variables with dynamically-scoped values for this, though you can work around the absence of first-class support as long as you have thread-local storage.
C++ actually uses this model for its out-of-memory handling. You can register a callback with std::set_new_handler() to be invoked if memory allocation with `operator new` fails; if it returns then allocation is retried, and only if there is no handler is an exception thrown. Unfortunately this approach didn't really catch on in other areas.
I agree, but I was not relying solely on callbacks. You do need some form of non-local return (such as exceptions or continuations) to implement the "resumable" aspect with a choice of restart points, in addition to the callbacks.
> You really need full coroutines in the general case.
I'm having a hard time imagining an error-handling scenario that would require the full power of coroutines—in particular the ability to jump back into the condition handler after a restart. In any case, most languages (even C, if you work at it) can express coroutines in some form or another.
> Anyway, what you are proposing is more of a partial alternative to exceptions, since the caller has to be aware of what's 'handled' in advance, whereas conditions may additionally unwind up to a predefined restart point or fail up to the caller similar to a non-handled exception.
Clearly there has been a breakdown in communication, as I though this was exactly what I described. The handler callbacks are "ambient environment" (i.e. per-thread variables with dynamically-scoped values) so there is no particular need for the caller to be aware of them unless it wishes to alter the handling of a particular condition. Restart points can be implemented by (ab)using unchecked exceptions for control flow to unwind the stack, or more cleanly via continuations if the language supports them.
> Traditional C++ exceptions have two main problems:
> 1) the exceptions are allocated in dynamic memory because of inheritance and because of non-local constructs like std::current_exception. This prevents basic optimizations like transforming a throw into a goto, because other parts of the program should be able to see that dynamically allocated exception object. And it causes problems with throwing exceptions in out-of-memory situations.
> 2) exception unwinding is effectively single-threaded, because the table driven unwinder logic used by modern C++ compilers grabs a global mutex to protect the tables from concurrent changes. This has disastrous consequences for high core counts and makes exceptions nearly unusable on such machines.
That's really interesting. I've become very anti-exceptions in recent years far various much-discussed reasons (eg hard to follow, false economy, difficult if not impossible to write threadsafe C++ code in particular, use of exceptions as flow control is an anti-pattern).
One of the porposals is a value-or-error type object, which is basically what Rust has. I really like Rust's enums and match expressions.
It seems so difficult to make changes like this to C++ at this point, at what point do you just have to start again?