The basic problem with C++ is that it has has hiding without safety. C has neither, and most newer languages have both. Attempts to add safety to C++ via templates always seem to leak raw pointers, since many APIs never converted to C++. It's significant that there isn't a C++ Linux kernel API, where you use C++ strings for everything and get rid of the null-terminated stuff.
You see this in the original poster's example. He's doing C-type I/O in C++. Of course it doesn't work cleanly.
It's frustrating. C++ is object-oriented. The cool kids don't like that. So, Rust isn't object-oriented; it uses traits and generics. Those are different enough that there is no one-to-one way to convert modern C++ into safe Rust. It takes a redesign.
So we're kind of stuck.
If you're familiar with game engines, consider what it would take to convert Unreal Engine to Rust.
C++ is a multi paradigm language, it is not “object oriented” necessarily.
also, there is nothing wrong with C++. it’s C with additional language constructs and the STL. use as much or as little as you want of it.
also, there is no such thing as leaking a pointer. you can leak a piece of memory.
at the end of the day you need to understand operating systems and how memory management, CPUs and instruction sets work and lots of other low level stuff to build software using C or C++. Rust might temprarily help with some challenges or shield off certain otherwise advanced challenges but in the end there is no way around learning how the machine you write software for works. if you don’t want to deal with that, go build websites.
Leaking a pointer occurs when you take a raw C pointer from one of the safe templated C++ constructs. This happens far too often because there are so many C APIs around that are still used from C++.
Generic, functional (with the introduction of lambdas and closures, in particular), procedural. Some of the features may use the class system under the hood, but with a very shallow hierarchy it's more akin to a trait/interface system if you drop the deep class hierarchy style that was more prevalent in the 90s.
I'd just look at the C++ Standard Library which relies far more on the procedural, generic, and functional styles than OO. Using it, you often (IME) end up writing code more along the trait/interface style than the OO-hierarchy style that was popular in the 90s. But I have no idea about open source C++ projects, I don't follow them and mostly don't care. I've really only used C++ at work where 99% of the code seems to be procedural, not OO. The closest is that they have "classes" which are barely more than plain data objects ("smart" structs), if they have methods it's to enforce data invariants and almost nothing else.
That's highly dependent. C++ is pretty bad and I've seen quite a few codebases where the C++ is way worse than the equivalent of Rust.
> there are hidden allocations EVERYWHERE that you can't even control
That's simply not true. It's also a highly ironic statement given the tendency of C++ to have all manner of allocations all over the place (example--look at how many people implement their own String instead of using the STL one).
You can control the allocations in Rust, but you do have to do so explicitly. Agreed, this is more like C than C++.
> stop trying to push that narrative, it makes rust look bad
Yeah, I kind of agree with this in the game space.
There are a LOT of things that Rust does poorly within the gaming space.
Being able to efficiently transmute between equivalent types of memory (array of 4 bytes, an RGBA packed struct, and a uint32_t, for example) without losing performance is something Rust is really bad at.
Packed structs are poor. There really isn't a good set of idioms for GUIs. ECS seems like a bit of a hack to get around the inability to transmute types efficiently. etc.
Oddly, one space that Rust is gaining traction in the gaming space is the network communication layer. People are simply tired of debugging buffer overruns and have decided that they'd much rather have something different. And, since Rust can compile down to a library, it can be linked into the rest of your game and not interfere with your pipeline.
> just take a nap and move on, you are delusional
Hey! You don't need to be delusional to take a nap. You can like naps just fine without being delusional.
I would say many (not all) game engines could probably be converted, by finding common pattern in usage of inheritance and mapping it to rust patterns.
EDIT: Through automatizing this, or making C++ to rust bindings for it would be hard.
I mean creating inheritance graphs for entities is as far as I know already seen as an anti pattern since a while.
Things like ECM are nicely re-presentable in rust.
Many patterns of inheritance for code re-use tent to also not be too hard to represent in rust.
But I would argue that even if rust had inheritance engines like Unreal Engine would not be ported, at least not until they do a major rewrite anyway. Because of it being a lot of work with not too much intermediate gains.
There's a pretty wide gap, though, between "nicely representable in rust" and "mechanically translatable from the C++ implementation" that mostly requires human effort to span.
Some of these ramblings are just plain wrong, like
> But exceptions come at a performance cost.
No they don't. An exception that isn't thrown costs nothing.
Also
> Let’s say I want to know if a constructor failed. I have two options, one is to pass
in an in-out parameter, the other is to the throw an exception.
Using a factory function instead is a third option that avoids this. Besides, this is a false dichotomy to begin with - it's perfectly acceptable to simply record the validity as part of the object's state and provide access to that (especially if the state established in the ctor isn't an invariant). So there's more than just two options.
Then there's more confusing arguments like the custom FILE wrapper. So instead of just using std::unique_ptr<std::FILE, decltype(&close_file)>, i.e. the idiomatic solution for the past decade that the author claims to be after, they blame their C++03-style on the language? I don't understand.
Fortunately they don't provide any concrete examples of "features bleeding into each other", since I suspect that would only further illustrate their lack of
understanding and knowledge gaps...
They end with
> I’m not in the business of writing the perfect program that satisfies an arbitrary standard. I’m in the business of making stuff with tools. [...]
Well great, then don't use C++ if you're not productive with it. Yes C++ is a complex language. A big portion of its complexity can be avoided by simply not using features one doesn't need (or understand) or by limiting oneself to a reasonable subset of said features.
The author seems to look for something like Go instead. C++ wasn't exactly "designed" and is the result of a steady evolution that kept some legacy baggage and bad earlier decisions around.
> No they don't. An exception that isn't thrown costs nothing.
Wait whaaat?
When every expression can throw anything, the compiler and the user both loose the ability to reason about code locally. They change any function from returning one value of one type, to returning any value of any type.
This:
- has a performance cost: it breaks lots of compiler optimizations that require knowing where and what can functions return.
- has a compile-time cost: compiler needs to generate code for landing pads all over the place, this code must be optimized, etc.
- has a code-size cost: the landing pads must be generated in the binary,
- has a usability cost: users can't reason about their programs if they can't reason about where functions can return and what they can return
- has a teachability cost: users have to be taught _a lot_ about how to use exceptions properly because of their cost. Herb Sutter has a whole damn series of books, a series of damn books, about how to use exceptions properly and avoid their pitfalls. When you need a series of books to explain 1 single feature of your programming language, that feature is _wrong_. And writing and reading these books has a huge cost.
Exceptions are as free as Google or Facebook. You can use them for free as long as you gift them your soul so that they can sell it for profit.
it's obvious that OP meant a performance cost - as generally when you program in C++ that's the n°1 metric. And for that one, exceptions as they are today are often faster than the equivalent error-code-based code: https://nibblestew.blogspot.com/2017/01/measuring-execution-...
Of course if we can get even faster exceptions such as the throwing values model, by all means let's do it... i'm entirely fine with recompiling literally every software / library I use if that can get me half a % more performance
The example on that blog post is not representative of how error codes are used.
On all programming languages, C included, but also Rust, functions return a completion value that's either success, or error (and when its error, it includes which error it is, and the error state, etc.).
int foo();
switch (foo())
case 0: break; // success
case 1: /* error case 1 */
case 2: /* error case 2 */
...
Instead, the blog post has the function return some value in registers that is not an error, and passes the function an ErrorState* as an argument, that the function first sets via memory, and then the handler reads, also via memory.
That this is slow is not rocket science, which is why nobody does this.
I get why the author has to do it. If they were to do it right, error codes would perform significantly better than exceptions. Since returning the error or success is "free" (the function has to return anyways, and up to 2 registers are reserved by the ABI anyways).
Somebody mentions this in the comments, were as far as fixing it and sending a PR (https://github.com/jpakkane/excspeed/pull/1) but the author complains that this is not how GLib does error codes, so they don't care.
So yeah, if you do C error codes like GLib does, which is essentially a poor man's emulation of exceptions, then sure, exceptions are faster than doing error codes wrong.
Most people using C do it right, and so does Rust's Result (and all Rust programs).
> The author seems to look for something like Go instead
But in Go you also have to write exception safe code, something the author complained about.
Before someone says "but you're not supposed to throw or catch in Go": the std http server swallows exceptions thrown in handlers, and fmt.Print does for String callbacks too.
And before someone says Go doesn't have exceptions: It does, 100%, in all but name.
I actually find C++ to be way more designed, though through, and consistent than Go.
That's not to say that Go isn't a vastly simpler language. It is. And C++ has way more "just because you can doesn't mean you should".
> And before someone says Go doesn't have exceptions: It does, 100%, in all but name.
But here the name actually matters. They're called "panic"s, not exceptions. When you panic, you signal a fatal error that should ideally terminate the program. You shouldn't care about closing files or freeing memory, because well-written Go does not panic, and if it does, execution will end very soon. A panic in a String() method is just bad code, don't do it. Meanwhile in C++ you can reasonable expect an exception anywhere. Bad arguments, invalid states, IO failures, and other errors that should be expected in the execution of a program all throw exceptions in idiomatic C++.
no, what matters is the language semantics. It could call itself "bamboozle" or whatever, the only thing that is relevant is what the compiler and CPU will do when reaching that code.
> idiomatic C++.
idiomatic <anything> is I think a big anti-pattern, as it creates assumptions and religious wars where there should not be. You've got a tool, you can use it in any way that fits the situation you're in. Is using a hammer to put a screw in a wall an idiomatic way to use a hammer/screw ? most certainly not. That does not mean that when you just have a hammer and a screw available you should throw your hands up in the air and complain that the gods put you in un-idiomatic situations.
> But here the name actually matters. They're called "panic"s, not exceptions.
But crucially that doesn't at all affect what they do, how they do it, or what it means for programmers needing to write code.
Even if you never write "panic", or never write "recover", your code still needs to be exception safe.
> When you panic, you signal a fatal error that should ideally terminate the program
The "should ideally" is the flaw in this plan. If your code could EVER be called within an HTTP handler, or from a String() function, then you NEED to write it exception safe.
Go authors explicitly say you should NOT count on finalizers for correct behaviour. So no, close your files.
> well-written Go does not panic
But sometimes it does. And if your code is not exception safe then you turn a safe problem into, essentially, undefined behaviour.
> and if it does, execution will end very soon.
Nope, not if panicing in an HTTP handler. If these two HUGE examples I've given you didn't exist then I would likely agree with you.
But I'm going to declare knorker's law: Every piece of code ever written will at some point be called from an HTTP handler.
> A panic in a String() method is just bad code, don't do it.
That's too much "just don't write bugs". Modern languages are supposed to be safe, no?
> in C++ you can reasonable expect an exception anywhere
Yes, just like in Go you never know in what context your code will run in the future. Which means that in both Go and C++ you need to write exception-safe code even if you never throw or catch exceptions.
To add to "Let’s say I want to know if a constructor failed."
Constructors are simply not meant to fail. You should never do something like io in a constructor. For everything more complicated than setting some fields a factory is better suited.
Why? Isn't one of the ideas of C++ that every constructed object is in a valid state? Even the STL has constructors that can fail, and it does it through member variables/flags, which doesn't seem like a very good solution:
std::fstream s(filename);
if (!s.is_open()) {
std::cout << "failed to open " << filename << '\n';
}
No one should ever have to use factories, they really suck. Too much boilerplate, too much code duplication.
> Constructors are simply not meant to fail. You should never do something like io in a constructor. For everything more complicated than setting some fields a factory is better suited.
How would you reconcile that with the fact that std::vector<T>::vector() is explicitly permitted to fail? [1] Do you construct your std::vector<T> objects with factories too?
> Exceptions: Calls to Allocator::allocate may throw.
Yeah but that's completely avoiding my point. You're claiming constructors aren't supposed to throw, but I'm pointing out the standard explicitly lets vector's constructors fail, and there's no other mechanism to handle that failure than by catching the exception (it's literally designed that way). It's fine if you prefer not to use those constructors in the first place, but that's kind of beside my point about what constructors are "supposed" to do.
> but I'm pointing out the standard explicitly lets vector's constructors fail
Yeah, but that thing has more than one constructor. Since C++/17, the default constructor of std::vector which doesn’t allocate any storage is usually marked as noexcept, i.e. the standard does not allow it to throw anything. This feature allows to make classes which contain vectors yet don’t throw exceptions from constructors.
P.S. The move constructor of std::vector is marked with noexcept as well, i.e. it’s even possible to construct non-empty vectors without any exceptions.
Using a function pointer as a unique_ptr's deleter type, and passing it in at runtime, makes the unique_ptr fat (two pointers large). The "idiomatic solution for the past decade" is a lot more complicated than you make it sound.
To avoid this overhead, I prefer passing in a default-constructible type with an operator() deleter function, and not passing in a value into the unique_ptr constructor. A neat party trick is to use the decltype of a lambda as such a type (https://old.reddit.com/r/cpp/comments/rlvsq0/does_anybody_re...).
I agree that C++ is a mess of a design, but I don't think this article quite gets how. RAII and move semantics are two of C++'s best features, and the ones that any replacement - like Rust - will copy.
To me, bigger flaws with C++ are that it's too big. The language is bloated with multiple features that accomplish similar things, but each with their own odd corner cases and interference. Anytime there's an issue from a language component, the solution is to tack another language component on top of things. See for example the progression of Macros -> Classes -> Templates -> Constexpr -> Concepts. Each tried to fix flaws in the previous design, but each introduction made the language more complicated, not less.
> But exceptions come at a performance cost.
I mean it's slow to throw, but non-throwing code paths are hardly affected. Typical implementations store the exception unwinding stuff off to the side, so the only cost is binary size.
> That means you need to write code that wraps every possible resource in RAII logic.
Consider using scope guards. You can write them directly in your code, without a separate class per resource.
int* ptr = malloc(...);
auto guard = defer([&]{ free(ptr); });
> As of C++20, the right way to write a trivial getter in C++ looks like `[[nodiscard]] constexpr auto GetFoo() const noexcept -> Foo { return foo_; }`
C++>11 is such a mess than most useful changes are invisible to most users due to bloat.
I think it's way past time to call it quits and abandon active development of the language. New features will come from other languages without 35 years of baggage.
I'm not sure it is remotely reasonable to choose a language for a project based on the verbosity of accessor signatures when being maximally pedantic.
C++ is indeed a mess but there is almost no alternative for existing enormous codebases than to keep making it better. The industry will slowly adopt memory safe system languages, but that's going to take ages to go beyond greenfields and absolutely security-critical systems.
I do think the article's criticism of C++'s move semantics is fair. Rust's "copy" of both RAII and move semantics is drastically simpler, and C++'s version is part of what makes the language so huge.
No. The FILE example is either bad, tired, lazy or shows lack of knowledge of the standard library. A better way of opening/closing the FILE with RAII is right in the example [0] of unique_ptr at the wonderful cppreference.com:
std::ofstream("demo.txt") << 'x'; // prepare the file to read
{
using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;
unique_file_t fp(std::fopen("demo.txt", "r"), &close_file);
if (fp)
std::cout << char(std::fgetc(fp.get())) << '\n';
} // `close_file()` called here (if `fp` is not null)
I'm sorry, I'm not trying to be negative towards the author, but you need a better example than opening/closing FILE if you're going to make a compelling argument about the looming death of C++.
But this also shows a lot of warts that mean RAII isn't "really working", as in it isn't reducing the mental load of resource management as far as we'd like.
- You've needed to manually specify the deleter. Why do I need to invent unique_file_t myself? The stdlib is failing to support idiomatic language use.
- Probably most critically, fp can be null! If A is I, then I is A, yet here we are with a resource in-scope but not allocated!
- You've had to manually add a scope to handle it, and (unlike e.g. Python with or Java try) there's nothing per se to indicate that's what the scope is for. SBRM is supposed to be interesting because of how often our scopes align with lifetimes naturally; manually adding a scope isn't too far from manually calling a destructor.
Please don't respond with the reasons "why" it is this way; we all know. That also doesn't change that they are still major warts that undermine safe, clean C++.
> - You've needed to manually specify the deleter. Why do I need to invent unique_file_t myself? The stdlib is failing to support idiomatic language use.
But you're not supposed to use fopen / fclose at all, in unique_ptr or not, so the language shouldn't encourage you to use those.
They are just often shown in unique_ptr, because when making examples to teach the language, it's a quick and easy way to showcase how to wrap any kind of pre-existing handles, not only memory, through the unique ownership semantics of unique_ptr; any other example would be platform specific (say, HWND on Win32, GL contexts or who knows what). Everyone knows fopen/fclose.
Basically, that unique_ptr thing is just here to show that it's possible to quickly ease porting 45 million lines of C to C++ without having to change every fopen into an ifstream/ofstream.
His point is that C code often calls into C++ code. This is always incorrect unless the C++ entry point is guaranteed not to throw an exception. This is especially bad since this is not automatically checked by default.
OP is trying to create a circular queue by using malloc(). This may be a good learning exercise, but the code could be made much simpler by just wrapping `std::vector`. Working with raw uninitialized memory is difficult in any language.
Most of the article was about C++ exceptions specifically, and I agree C++ exceptions are probably "doomed." They are already disfavored by major codebases (Google, LLVM, others). It is indeed unreasonably hard to make exception-safe C++ containers.
One possible future path here is Herb Sutter's Zero Overhead Deterministic Exceptions [1] which "throws" values through the ordinary return path, as is done in Swift/Rust/Go etc.
I don't know how `bad_alloc` will get handled, but I'm not sure how useful that even is any more, given Linux memory overcommit. Curious to know if anyone takes bad_alloc seriously?
> Let’s say I want to know if a constructor failed
Usual advice here is "don't write fallible constructors." Use a factory function instead.
I can't agree with some of the other points. I don't see how "copies that result from calling a function have a different meaning to regular copies." RVO became guaranteed in C++11, and you don't need to worry about value categories when implementing move or copy constructors. C++ is not an easy language but it does have rules. And most of the time this stuff can be ignored: if I copy a string an extra time it usually doesn't matter.
Gonna be a long while before safety and certification bodies for industries like aerospace move away from C++. All the tools for the past few decades focus on C++ as the main systems language in these cases.
Rust is great as a spiritual successor with more stable performance (in general) but the institutional inertia with C++ is strong.
In this aerospace company, we use C, no C++ allowed, to avoid all the extra footguns that get in the way of safety and certification. Rust will probably be the next choice on the scene, but I expect it'll take as long to come as Ada took to go.
In this aerospace company, we use C, and sometimes a subset of C++ that is basically C+epsilon. That's all new, however, so each new project that goes that direction is walking a dark forest.
We've got some Rust on the ISS, but honestly, the interest from the serious FSW folks isn't there.
> Rust will probably be the next choice on the scene
I'm not sure about it.
I think Rust might be hard to certify for aerospace usage.
Not because it's bad, but because it's still complex. Just a lot of negative side effects of complexity are contained due to the compiler checks and design.
But that makes it harder to certify as I can tell.
Ada relied on colleges teaching it or Pascal, so then it became hard to hire. I understand the rationale behind this, but the language is surprisingly easy to learn because it's usually explicit and straightforward.
It suffered from two decades of bad press due to being forced by the DoD and then almost two decades of no press. Looking at Google ngrams, C (due to Unix) took big chunks out of it and then Java gave the death blow by becoming a major teaching language. Ada 2012 had the capability to do for Ada what C++11 did for C++, but it wasn't sold well.
I picked up Ada 2012 last year and made a tool I use everyday now for real work--it's not a bad language. It suffers from a lot of myths about it, but the tool modernization like getting a package manager makes it productive. It is "boring" in the sense that there's nothing flashy about it, and its surface verboseness which saves other code later, and lack of curly braces turns people off. The community is super small and nice, but it never learned to sell the benefits of the language.
I only have limited experience, but I’ve seen projects migrate from Ada to C++ more often than the other way around. If I had to guess why, it’s probably because it’s easier to hire for.
What's the state of Rust/C++ interop currently? Naively it seems like allowing Rust in C++ projects could be a reasonable way forward, but I haven't tried it personally yet.
It's fine, works well enough. Challenges are passing data across the boundaries as the data types need to conform to both sides. E.g. using a string in both languages without doing copies means using a C-style string and now you've lost some expressiveness in both languages.
Second challenge is smoothness of integration in build systems. In C++ you might use CMake, in Rust you use Cargo, so now who is authoritative? Do you use Cargo to build the C++ code? Do you build your Rust projects in CMake?
I think you're implicitly saying that Rust solves the problems in the article, and so migrating C++ projects to use Rust is a path forward. However, the blog author's first example was implementing a "contiguous circular queue", so you should try to do that in Rust. Of course Rust already has VecDeque<T>, but then C++ already has std::deque<T> too (although not promised to be contiguous). So the exercise is to implement your own, from scratch, without just wrapping a type that does the dirty work for you.
If you look closely at the implementation details for VecDeque<T>, you'll find a lot of complexity you might not expect. To do it efficiently and correctly, you need to work with unitialized memory. So there will be unsafe blocks in there. A VecDeque<T> is built of a RawVec<T>, which uses a Unique<T>, which finally has a pointer, but also contains a magical PhantomData<T>. Look at the code [0] [1] [2] [3], look at the implementation details, read the comments, and assess for yourself if it's easier or harder than C++.
Later in the article, he talks about exception safety. For something like a container, the most likely exceptions are that you've run out of memory, or you can't move/copy/construct an item in the container. I'm honestly not sure how Rust's builtin containers handle these problems. (Panic?) But if you're comparing the two languages, the apples and oranges matter.
All of this to say that Rust is not simple either. You can program Rust by using its standard collections, and you can do the same with C++. If you try to implement those collections, say for learning/teaching data structures, both languages seem painfully complicated to me.
> Later in the article, he talks about exception safety. For something like a container, the most likely exceptions are that you've run out of memory, or you can't move/copy/construct an item in the container. I'm honestly not sure how Rust's builtin containers handle these problems. (Panic?)
As I understand it yes, the containers will panic.
Part of the discussion about using Rust in the Linux kernel is what to do about failed memory allocations. And this is a problem more generally for Rust usage in embedded systems.
So there may need to be support for alternative allocators or something else.
Part of the key is that they’re not really “built in.” They’re in the standard library. When you’re in those contexts, you just don’t use the standard library. Problem solved.
Now, the standard library is also getting support for these things not panicking on allocation failure, and when that’s ready enough for those that want it (Rust for Linux has pulled the changes into their tree, in my understanding) then they could use them.
(I work on embedded systems with no dynamic allocation, Rust is great.)
Ada never really "won". It was mandated by DOD, but the industry more or less rebelled at the idea. Fortran was pretty common, but C and C++ are the dominant languages. The last greenfield aerospace (avionics) project I was on, we were given a choice and it seemed that we (and everyone else) went with C or C++. There are a lot of static analysis tools now that cover most of what you get from Ada (and then some, even with SPARK), but the choice was really based on familiarity for potential new hires. You also use strict subsets of C or C++ in this field, not every feature will be available. (I wish it had been in Ada, a lot of issues in the system wouldn't have happened in Ada, but they did eventually get worked out anyways.)
I'm not in the industry but from what I know it's a split between Ada/C/C++. More space related applications tend to be C/C++ (satelites, rovers, etc.) while the aero-side uses more Ada (Boeing, Airbus etc.).
b is a limitation. C++ makes writing good general code a nightmare so you reduce to taylor your data structures to your problem
c is just folk wisdom. That quote is just untrue
a is true but it has gotten to the point that implementing C++ std libraries and compilers is so daunting a task that even the big vendors do not rush into it anymore. There is this growing fatigue that left only MSVC, gcc and llvm in the game, and even them are always late to the standard
Nothing folk wisdom about it. It's not 100% true but the quote is meant to highlight that most of the languages that get highly praised tend to the ones where the only people using it are those who really love it. People don't use C++ or JavaScript or other high demand languages because they're such wonderfully designed languages that they love, they use it whether they want to or not because it's the industry standard. Compare that to people who write Haskell, or D or other niche languages where the only people using those languages are the ones who really like it. If people had to use Haskell or other smaller community languages because they somehow became industry standards, I assure you'd be hearing tons and tons of complaining about performance issues, how buggy they are, how annoying it is to do X, Y, Z...
As is you don't hear people complaining about Haskell or D because the people who find those languages to be annoying simply stop using them and there's little reason to write a blog post about it. The predominant reason to write a blog post about a niche language is to basically give it some degree of praise.
>There is this growing fatigue that left only MSVC, gcc and llvm in the game,
What do you mean by this? I'm not aware of any other language that has three independent implementations that are all kept up to date within a year or so, the only language that comes close would be Java but even that is mostly dominated by Oracle's implementation.
To the point of complexity, the allocation makes the container unfit for over-aligned types. But honestly I don't even know if STL containers are over-alignment aware.
So instead of
(T*)malloc(sizeof(T) * capacity_);
it should be something like this if you want to deal with over-aligned types.
Also, in the reallocation function the elements are move-assigned to uninitialized memory. They should be move-constructed with a placement-new. And of course calling the destructors of the moved-from objects is still required.
It's of course a little telling that the compiler will not complain about these errors and you have to use and know to use tools like address sanitizer, memory sanatizer and valgrind to let a machine help you find them.
On a long enough timeline I do believe C++ is doomed or will otherwise become something completely unrecognizable from its current form. Similar to how C++98 code is wildly different than modern C++ code. People often ask, "is C++ a good language?" or "is XXX better than C++?". Doesn't really matter, what matters is whether people are willing to pay the "tax" of learning/using/maintaining C++. C++ is a generalist language and new languages are competing with it by specializing in different areas. If I am going to use something as unergonomic and perilous as C++ it needs to be for some advantage but looking into the future and comparing to newer language, that advantage is becoming less clear.
C++ is not a generalist language, its strengths are high performance, direct memory access and native compilation. If an application doesn't need any of that, a higher level language is usually better.
Something that I have learned is that we build abstractions to hide complexity. In so far as abstractions are not leaky and are fast, all is good. The moment you have a bug, or the moment you want to analyze performance, abstractions absolutely puke their f'ing guts at you and all that smart stuff someone thought no one would ever see spills all over the floor and it's a flat-out mess. C++'s pervasive unsafety and pervasive obsession with performance are the worst kind of witch's brew for plunging you straight into a hell of unimaginable depths at the slightest mistake.
In particular, undefined behavior is so pernicious that it exposes literally everything about the machine and the entire program at once--its memory layout, compiler optimizations, and whatever else you weren't supposed to see. You are staring at a whole pile of hot garbage. There are literally no constraints on what the entire system is required to provide to you as aid to debug the problem. Undefined behavior literally gives no meaning to the entire execution of the program since its start, no matter where or when the undefined behavior occurred. There is no "before" UB and "after" UB--the entire execution is invalid. You are in a parallel universe that is not required to make sense.
Even if you look at it as at a strictly legacy technology, this legacy will be around for a looong time. A lot of important code is written in C++, without any viable replacement.
Also, like Fortran, it will keep improving even while relegated to its niche, and will become much nicer (yes, even nicer than the latest standard).
I should note that writing data structures in rust is also a fraught business! In practice, that’s just a property of languages that don’t have garbage collection.
Exception safety is a serious complication in c++! Exceptions were a mistake in the language, particularly given the existence now of std::optional.
Finanally, c++ is the only language I would ever want to do linear algebra in, or for that matter, many graph algorithms. Rust is adding features to make it useful for linalg, but it’s not their yet.
Linear algebra is important!
Perhaps syntactically C++‘s operator overloading can make it quite palatable, but C and C++’s unique aliasing rules would mean that a naive implementation could leave performance on the table. There’s a reason it’s almost always Fortran at the bottom of the stack in most LinAlg codes in most languages.
that's just false. Exceptions were not a mistake and std::optional isn't even a good type to replace more than communicating that there is no value. It's as good as returning nullptr, in this case. And what strength of an exception guarantee does one really need? A basic guarantee is not that bad to give and most things do not need to worry if they don't use unmanaged resources without RAII types(e.g pointers).
But the big thing is, a large amount of code does not have error states at all, they always work. Another chunk have preconditions, that are generally developer errors, that shouldn't be throwing either. That leaves the parts that interact externally. And here is where one learns, it's not a dichotomy of exceptions or error flags/results... it can be both. It's more to do with the shape of the code.
One example is, one is passing a string buffer to a parser, an exception is probably fine as the error cannot be dealt with within the parser. It didn't create the malformed document and cannot fix it. To use error types will greatly complicate the inner workings of the library as it has to deal with errors at a majority of calls internally. This leads to more bugs(assuming more code == more bugs) and complexity/branches. One already paid for the branch to check for the error in the document too, now they have to pay for branching up the stack. Exceptions strong part is that they cannot be implicitly ignored. One can do that with error types that force a read prior to destruction too though.
For short distance things, where one is handling the error state after the call, not throwing is fine.
I believe it's inevitable. But I would have given a very different answer even a few years ago. What strikes me the most though is that C++ is that it's not being succeeded by a new better C++ but instead it's being slowly replaced by multiple languages at once. C#, Java, Go, Rust, Swift, and JavaScript, (and maybe even python) are all replacing C++.
In 22 if someone wants to make a general desktop app they'll probably use web technologies, or python, or java. If you do server development it's Python, C#, Java, Ruby, or Go. Embedded development is predominantly pure C. C++ is used but less often. Gaming is still a singular C++ affair but I'm starting to see C# make headway and now Rust is gaining traction. I've seen more game developers excited about Rust than I have about anything in the last 5 years.
You can see it happening slowly, as more and more technologies are coming out and chipping away at it's use cases. Unless you're targeting game consoles, or a very specific rare domain, using C++ might be more of a handicap with so much better options out there.
> In 22 if someone wants to make a general desktop app they'll probably use web technologies, or python, or java.
I wonder what apps you are talking about. The immense majority of apps I use are Qt. Just last week I was trying some newish Intel GPU profiler GUI: https://www.intel.com/content/www/us/en/developer/tools/grap... ; a Qt app. Last month I produced a video, with Da Vinci Resolve, also a Qt-based software.
On the other hand, on my whole desktop I literally don't have any Java apps installed at the moment, like, not even a JDK. There may be a 2012-era copy of Minecraft in some backup folder. Likewise for Python, at most some apps will embark Python scripting around a C++ core like Blender but I don't think I use any app where the main() is written in Python.
C++ has its problems, but it has two advantages to other languages: (1) it is evolving and extensible at a deep level: one can identify strategies and create solutions based on the template system, because that's how the standard library itself is created. (2) it is backward-compatible, which means that we can continue to support the incredible amount of software already available for C and C++.
While that is true, this in turn means that the result is complex. You have old and new ways to do things, and new things are complex with all the integrations into the different parts.
Unified initialization sounds great, always use brace initialisation and be done, you think ... but no it's complex.
Or move semantics and universal(?) references are introduced to solve efficiency problems and make things nicer. For simple stuff it works, but oh the mess becomes wild.
How well modules will work out is still to be seen. Suddenly you need the build system to be a compiler and can't parallel build everything anymore, but have to create a dependency graph to build in order, while the standard treats that as a implementation detail.
C++ has some great features, however it would need a big cleanup, a cleanup which would break all code, worse than Python 2 vs. Python 3, in a world where other environments and languages are viable alternatives. Thus C++ can't afford the big jump.
yeah but it sucks to become the new "cobol" - I hope not, at least, as I am learning now C++ after so many years and I really like it. But hey, I don't make money with it, so I can't talk about how frustrating it must be to debug some weird stuff, etc.
> I have two options, one is to pass in an in-out parameter, the other is to the throw an exception.
Another one would be not using constructors at all, instead implement initialize() or create() method in the class.
> Now you have the issue that every single operation can throw an exception
Depends on the code. I tend to avoid exceptions when I can.
> you need to write code that wraps every possible resource in RAII logic.
That’s generally a good idea regardless of whether exceptions are used or not. Without RAII for file handles, next day/month/year another programmer will write `if(condition) return;` and leak the handle.
> And when it comes to implementing move and copy constructors you are left feeling like you’ve definitely done something wrong
For many classes in my code, I disable both with =delete; copy constructor. This makes a class which can be neither moved nor copied, which simplifies things substantially. For instance, if you need a RAII equivalent of CAtlFile [0] over FILE* from stdio.h, immovable file handles might be a good idea. Another thing, if the class owns many gigabytes of data in some collections, you wouldn’t want to copy anyway, too expensive. In some cases I want to move such objects, for that I define a swap() method. An explicit swap() method is IMO more readable than std::move.
For many other classes in my code, I declare no constructors at all because the default compiler-generated ones already doing what I want.
There are 5+ million C/C++ programmers out there. Why? Because most of the world runs on C/C++. Even Java/Python/JavaScript etc. runs on C/C++ VM’s running on top of operating systems written in C/C++. Rust is nice but it will take a very very long time for it to have the kind of impact C/C++ has. If it ever happens.
When i do my periodic refresh runs c++, i often question myself - why i'm bothering with it? C++ seems more about programming c++ itself than programming against the actual problem. At which point i reason that if i ever need to go native and fast, i'd simply use C, and for the higher level use not-c++.
But in Rust the negative effects of the complexity are mostly contained by the compiler (i.e. if you get it wrong you get a compiler error).
In C++ it on the other side can easily lead to UB and in turn to potential security vulnerabilities.
So IMHO today there are _very_ few cases where C++ is still the appropriate choice.
Just to be clear I'm not saying rust did replace C++, but there is a wide variety of languages which are a better choice for many use-cases which once where filled by C++. Rust just further reduces use-cases where I would say C++ is appropriate.
But it's anyway not Doomed as there are just way to many existing projects. Not just legacy projects, but existing still maintained for many years projects.
No, just as Betteridge's Law would lead you to suppose.
This really boils down to "creating your own custom data object that plays nice in all scenarios is too hard". (Or, more cynically, too hard for the author - some of us don't find it particularly hard.) Does that mean C++ is doomed? No, it just means that not all C++ developers are going to create objects with full RAII initialization semantics and full rule-of-five acts-like-an-elementary-data-type behavior.
And that's fine! Not every object needs that. And if your object doesn't need that, you don't have to put it in.
The problem, of course, comes when you have an object that needs all that, and you don't know how to do it. But that's kind of true for every feature you don't know how to use, in every language. So are all languages doomed? No.
Now, you could make a case that creating objects that have all those behaviors is the ideal in C++, and it's too hard to achieve that ideal. But hyperventilating about how that makes C++ doomed doesn't do anything to actually make that case.
I can’t believe I had to come down this far to see the Law of Headlines. C++ isn’t going anywhere for a long time. For all of its technical and design faults and overbearing complexity, its (yes, deceptive) compatibility with C, the most important language in the industry, even at the source level is unmatched and it’s the only language that even competes in the space that it’s in: a language with all the power of classical OOP, which is the paradigm of the industry (hated or not), with the performance of C. It has many different implementations to run on pretty much any computer you could want to run code on. Its compilers are industrial strength and best-in-class and it has wide tooling support. There is a broad talent pool to draw from for hiring.
Rust is the only thing that even looks close to a competitor, but it still has a long road before it’s viably a replacement, and its lack of OOP and strict ownership rules may prove to be too foreign for the vast majority of programmers who are used to slinging pointers around and OOP design patterns, not to mention the cozy C syntax.
Even if C++’s technical merits are lacking, it won a cultural victory by catering to both the hegemony of OOP and C.
Then you can't allocate the object in-place (e.g. on the stack), and also you have to ban copy assignment/construction. Probably a more common solution is like what STL's own fstream does - don't throw an exception on construction, and have a "not valid" state.
Sorry to say it, but your C++ knowledge seems outdated. What the gp comment explained is perfectly viable due to (Named) Return Value Optimization is for. Compilers have been doing that for a while and it's mandatory since C++17.
Yes, C++ has started changing a lot about a decade ago, and despite what the naysayers claim, most of the changes (like this one) are for the better.
I know about RVO, but that's irrelevant here because a factory function returning an object by value does nothing to avoid needing to throw exceptions if construction fails.
* RVO has always confused me. It was much simpler in the C++03 days. Copying large objects by value was inefficient. So you always used references. Period.
* Don't fully agree with the commentary on exceptions; it's the same in most other languages if you want resources to be freed/given back.
As someone who uses both Rust and C++ frequently with a good understanding of both languages, I can say that Rust is for the mostpart more pleasant to work in, but Rust's handicap is that it makes working in raw pointers when you really have to quite painful.
If you need full raw pointers with aliasing mutable pointers, the fact that fn drop() takes a &mut T is a problem that can only currently be worked around with fugly hacks, like nesting your data type in another pointless structure, not to mention the giant pile of ambiguous UB that you risk whenever pointers and references interact that could so easily be avoided if Rust would just implement an -fno-strict-aliasing switch, but the devs refuse on what appears to be ideological grounds. The result is that for unsafe Rust, you get about the safety level of C much of the time, because RAII and many language facilities are not safe to use. Unsafe Rust is currently so dangerous that I feel much safer reaching for C++ for some of it. Mutable reference uniqueness, I think, should be enforced by the borrow checker and NOT by pain of inescapable possible undefined behavior at the assembler level.
Now for C++.
C++ is lacking a LOT of useful safety features that Rust comes with baked in, such as Mutexes owning their data, the borrow checker, a good, fast atomic reference counting type, and things that translate to more safety indirectly, like algebraic enums, assignable and movable arrays, tuples built into the language, etc. Generics in Rust, however, are significantly more painful to write due to the lack of duck typing. Perhaps the Rust devs felt that duck typing was too "dangerous" for templates, but because they're statically evaluated, these issues almost never materialize, and so in practice duck typed templates are almost always superior to Rust's generics.
C++ is definitely more mature and pragmatic as a language, but Rust is probably better for systems programming overall, at least if they fix their goddamn unsafe UB stuff.
As for move semantics, that book isn't for "what you MUST do to keep things semi-safe!" though it contains some stuff to that effect. It's more about optimization and tricks from what I can see. Move semantics take a little while to wrap your head around, but once you know the basics of C++'s move semantics, you can pretty much figure out what's going to happen by looking at a class' definition briefly.
I dunno if it's doomed or not, but I picked up the language again recently for a project of mine and whenever I look at cppreference.com for any of the newer features I feel like an idiot. Feels like studying for college exams all over again. Smart pointers is as far as I'm gonna go with the post C++03 features, miss me with all the template stuff and compiler tetrises.
Hmm… I’ve been hearing “C++ is dead!” For twenty years or more. Same with PHP, but not quite as long.
C++ is not a language to be used for the vast majority of programming tasks.
It’s basically an industrial tool; for doing industrial things.
There’s a reason that industrial tools aren’t available in your local hardware store.
There’s some things that it’s best suited for; namely, doing big, complicated algorithms, very quickly. It’s flexible enough to afford almost any programming paradigm. Otherwise, I don’t see much of a use case for it.
There’s plenty of other languages, better suited for things like Web service delivery, or GUI programming. I used to use it for GUI programming (Mac programming, using CodeWarrior). It was not the best tool, and I don’t miss it, at all.
These days, I’m not even sure that I’d recommend it as an appropriate language for operating system development.
I did run a C++ image processing pipeline (big, complicated algorithms) shop that used C++, for twenty-five years, and saw it used for what it’s made for.
C++ is doomed, but it'll add so many features to mimic the cool new things that it'll cease to be C++.
TFA: "That is, of course, until you realise the design philosophy that is being followed. They literally just implement whatever the current fad in programming is."
No, they're fundamental to how C++ is idiomatically used. "Resource acquisition is initialization". This means that, if you have an object, and creating that object can fail, then it should fail in the constructor. So you either have a valid object, or none at all - you never have an invalid object.
Now, you could squint and say that an optional<T> returned from a factory is a valid object; that is, it's a valid optional<T>. I can sort of twist my mind that far, but it's not idiomatic. (It might become so, I suppose...)
This just shows what a bad idea RAII is. You're destroying opportunities for managing resources in bulk and coupling that to just gathering a group of related data under a single identifier.
Certainly you can write C++ for MCUs; the question is, should you, and does the ecosystem encourage that.
OTOH a bigger ARM core can very well be found on a small embedded computer, and it's capable enough to run something like a browser. At this scale, pure C becomes burdensome, and libraries (like a browser engine, or Qt) are in C++ anyway.
In the case of Arduinos? Yes. As I said, the original Arduino IDE has partial C++ support. Classes are used by almost every library. The AVR series originally targeted are 8 bit MCUs, and they run fine despite the ATMega328p, for example, having only 32 KB of flash.
> That means you need to write code that wraps every possible resource in RAII logic.
This is a great point. It means that you cannot mix C and C++ code without care, which seems like a huge design smell to me especially since this is not something that can easily be verified statically.
Using C++ with exceptions disabled is the only foolproof way to run it in a large heterogenous application.
I hope so, because I irrationally decided I didn't like it versus Objective-C a long time ago and I can't wait to be vindicated :)
But even if it is doomed, given inertia it will still be a mainstay of HPC and such long after Rust or whatever takes the crown as the language people take the time to complain about, to paraphrase Stroustrup.
In my C++ code, I've copied Rust and expressed fallible initialization by returning a std::optional<T> from a static method (which the article failed to mention). The problem is that (like Rust) you lose placement initialization, and (unlike C++ or Rust) you can't initialize private fields using aggregate initialization or initializer lists, and must write a passthrough constructor (which can't even be private because it breaks make_unique).
https://github.com/hsutter/708 is a C++ proposal which unifies placement constructors and writable out-parameters ("definite first use"). I don't think it makes placement initialization fallible, but I'm not sure.
Define “doom” or “death”. Cobol still runs, FORTRAN still runs, not the coolest in the TIOBE index but in the tail. A mammalian body doesn’t die all at once, either, but past some threshold of vitality does definitively die and remains dead. Really unclear whether these languages will be dead until the last compiler stops compiling. So the discussion needs a definition for the threshold the language may reach.
Have you heard the phrase "death by 1000 cuts"? That's what programming in modern C++ is like. I agree they don't quite make the case for "doom" but it's still not a pretty picture.
This image is mostly painted by people who forgot to stop.
By that I mean stop using every feature and detail because it's there.
C++ can be used like a much better C (smart pointers, references, const-correctness, generics via templates) and most of the complications can be ignored that way.
Premature optimisation is another big factor. If there's no measurable performance bottleneck, things like move-semantics don't even matter in practise for example.
Does the "much better C" include constructors are not? Smart pointers (which you include) are primarily designed for types with ~Type destructors, and difficult to use with C-style types with free functions for deallocation. And ~Type is difficult (unsure if impossible) to use without constructors, through either new Type or placement new into malloc'd memory. There is the trick of defining a passthrough constructor and moving initialization logic to a separate function calling the constructor. But if you want placement initialization, I'm unsure if C++ allows the C trick of casting malloc'd memory into Type * and passing it into a placement-init function that isn't a Type() constructor (pretty sure it's illegal to do so if Type contains std::vector, or shared_ptr, possibly unique_ptr). And constructors cannot report failures outside of out-parameters or exceptions, both of which were criticized in the article. So "most of the complications can be ignored that way" isn't true in my experience.
If you want fallible placement initialization, perhaps you could define a static method or free function taking a void* to malloc'd memory, which performs some logic then conditionally calls a placement constructor on the void*, then [[nodiscard]] return whether you succeeded. I find this awkward and not much better than C. Personally I give up placement initialization and return a std::optional<T>.
>references... generics via templates
Implicit copy constructors are a footgun not found in C, and references decay to copies at every opportunity (auto, templates).
I don't understand the obsession with constructors.
Placement new exists, but I'm not sure that's what you asked w.r.t. placement initialisation, which doesn't make much sense for non-trivial types anyway.
Fallible placement initialisation into allocated memory is something that I honestly don't know a good use case for. It's certainly not something that's very common and I never encountered it.
It's just one of those things that smell like a design that doesn't suit the problem. Do you by any chance have an example for when this would be useful? I'm curious to learn about this.
Implicit copy constructors can be disabled if need be by simply deleting them (Type(Type const&) = delete). It's up to the programmer since the compiler simply cannot know. As far as auto and copies goes: almost always auto&& and when in doubt, spell it out (i.e. don't use auto).
Again, not knowing the semantics isn't really an argument against the language. It also again boils down to a case of premature optimisation if copies concern you even though there must be a profiling log pointing to that being a problem first.
> I don't understand the obsession with constructors.
The article describes how smart pointers are more difficult to use for opaque/POD structs without destructors. Correct me if I'm wrong, but I think types with destructors generally require constructors too (though you can sometimes rely on default destructors/constructors, by holding fields which themselves have destructors/constructors).
> Fallible placement initialisation into allocated memory is something that I honestly don't know a good use case for. It's certainly not something that's very common and I never encountered it.
Understandable. Rust lacks any safe means for placement initialization (even infallible), resulting in stack overflows (often disappearing in optimized release builds) when allocating large heap arrays (because it has to stack-allocate the array, then memcpy to the heap), eg. https://github.com/rust-lang/rust/issues/53827. I haven't encountered this firsthand though.
> It also again boils down to a case of premature optimisation
Copying can be a correctness issue as well as performance. One time I wrote a function which took an object by copy (when I should've passed by reference), and returned a reference to the object, resulting in a use-after-free. Additionally mutating a copy doesn't mutate the original object, unlike a non-const reference.
> I think types with destructors generally require constructors too
If by that you mean explicit constructors, then no. The compiler will happily generate an empty default ctor for you. Ctors in general are only useful to establish a known initial state and uphold class invariants.
If neither of these applies (e.g. no class invariant or delayed initialisation), no ctor is necessary (or even wanted for that matter - initialisation is not free).
> Copying can be a correctness issue as well as performance.
Agreed, but that is just a matter of language semantics. Take the opposite for example: in Java every non-primitive is passed by reference. You get the opposite problem and now have to deal with (sometimes implicitly) boxed values and cumbersome cloning-interfaces. C++ has references and (const-) pointers and it's the programmer who decides when to use which option.
The language itself doesn't limit or restrict the programmer in that regard and unlike Java it's more consistent in its semantics (i.e. no ominous "value types" that require boxing and are not passed by reference).
This is not how programming works in large corporations. If a feature is there, the people reviewing your code will want you to use it. Even though most C++ shops have a specific subset of "allowed C++", the language itself nudges you to think about copying stuff - that's why it ha all the RVO, move semantics, ptr stuff inside. Having switched to Go recently, it feels so good when my teammates don't care (and don't know) that the variable in for-loop is always copied. We just care about solving the problem. In C++ you have 4 different ways to declare a for-loop variable, and you need to think about selecting the "correct" one, even if you're not doing performance critical code.
I think the first step is to explain to people that C++ is just C with more stuff, the original is way simpler and the ONLY real reason to use a C++ compiler is that most dependencies that you need (OpenGL, OpenAL etc.) uses C++ by default.
That took me 3 years to figure out on my own, such a waste of time.
In C++ defense namespaces, strings and streams do have some usefulness to them, but if I could use a C compiler to build a modern 3D MMO I would remove those in a heartbeat!
Edit: I know there are construed ways to call the OpenGL API directly without wrapper, but GLEW (damn Windows) is too comfortable for me to switch yet... Eventually I might switch to TinyC and write my own OpenGL/AL... That or implement my own J2ME!
OpenGL, at least, is a C API, not C++. If you're using it directly, there is no need to use a C++ compiler. EDIT: And I just double checked, OpenAL is also a C API.
Simple, it's a war between those who are Pascal inclined (and probably liked Macs and Steve Jobs) e.g. (https://www.quora.com/Why-does-the-type-go-after-the-variabl... - add Rust, Kotlin and Scala) and those who know the best languages, the ones that are actually used in production at companies large and small worldwide: C, C++, Java and C#. IOW, if the language does "type varname" then it's correct, if it does "varname: type" then it is doomed. It's sarcasm but it's true.
I used to be a C++ Fan Boy. I just started learning rust last week and noticed how old c++ is. All the good practices are built into rust that you need to specifically write in C++.
You see this in the original poster's example. He's doing C-type I/O in C++. Of course it doesn't work cleanly.
It's frustrating. C++ is object-oriented. The cool kids don't like that. So, Rust isn't object-oriented; it uses traits and generics. Those are different enough that there is no one-to-one way to convert modern C++ into safe Rust. It takes a redesign.
So we're kind of stuck.
If you're familiar with game engines, consider what it would take to convert Unreal Engine to Rust.