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.
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.)
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.
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.
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'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.).
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.)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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++.
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.
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.
* 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.
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++.
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++.
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.
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.
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.
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.
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.
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.
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.
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.
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.
> 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++.
> 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.
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.
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...).
> 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).
167 comments
[ 4.2 ms ] story [ 228 ms ] threadRust is great as a spiritual successor with more stable performance (in general) but the institutional inertia with C++ is strong.
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?
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.
[0] https://doc.rust-lang.org/src/alloc/collections/vec_deque/mo...
[1] https://doc.rust-lang.org/src/alloc/raw_vec.rs.html#52
[2] https://docs.rs/ptr/0.1.0/src/ptr/lib.rs.html#39-47
[3] https://doc.rust-lang.org/src/core/marker.rs.html#679
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.
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.)
We've got some Rust on the ISS, but honestly, the interest from the serious FSW folks isn't there.
But it's going to take a lot of time and work.
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.
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 thought Ada is prevalent in this space.
a) most C++ users don't implement stuff that goes into the standard
b) you can tailor your data structures to the problem at hand instead of making it usable with every type and use case
c) quoting Stroustrup: "There are only two kinds of languages: the ones people complain about and the ones nobody uses".
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
apparently the quote is confirmed by the man himself, though: https://www.stroustrup.com/quotes.html
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.
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.
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).
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.
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.
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).
I think "dethroned" or "unpopular" would be more correct presuming that C++ would lose popularity over time due to the issues raised.
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.
I also think that Zig has a good chance to become popular in the MCU space.
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.
Exception safety is a serious complication in c++! Exceptions were a mistake in the language, particularly given the existence now of std::optional.
Isn't something like MATLAB better for linear algebra than C++?
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.
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.
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.
Generally for any language which in design isn't "somewhat close to C++" I don't think this would work.
compilation time is even worse than c++
there are hidden allocations EVERYWHERE that you can't even control
stop trying to push that narrative, it makes rust look bad
rust is nice if you target things like drivers, anything else it's bad
the other day i read people who thought rust could replace javascript
just take a nap and move on, you are delusional
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.
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.
"IS CANDY DOOMED?"
I wish. Unfortunately I think it is we who are doomed and stuck with it forever.
This seems like the beginning of a bad situation. I mean, this shows why constructors that do any complex work that can error out are a bad idea.
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...)
RAII is a mess in certain circumstances (looking at you, Vulkan API) and sometimes straight up just doesn't work anyway.
If your initialisation is prone to failure, don't do it in a ctor.
* Move semantics itself requires a whole book https://www.cppmove.com/.
* 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.
These are not the only options. You can also return `optional<T>` or an other wrapped value from a factory function.
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:
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++.[0] https://en.cppreference.com/w/cpp/memory/unique_ptr
- 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++.
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.
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.
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.
Joshua Kerievsky talks a bout this problem in his book [Refactoring to patterns](https://www.programmingbooks.dev/#refactoring-to-patterns), specifically the refactoring "Replace Constructors with Creation Methods"
That is maybe beside the point of the articles argument he is making about C++ being bloated and hard to use and read ¯\_(ツ)_/¯
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.
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.
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.
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.
Also, that cookie pop-up on WordPress gave me cancer.
So instead of
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.
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.
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.
[1] https://en.cppreference.com/w/cpp/container/vector/vector
I usually do it like that:
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.
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".
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++.
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.
This code is a time bomb:
To function correctly it must be: > You shouldn't care about closing files or freeing memoryBut I do care about deleting tempfiles, unlocking mutexes, closing channels, etc…, because you do need to do that.
And this is the Go authors themselves showing off panics, saying "there's a bug" if the file isn't closed:
https://go.dev/blog/defer-panic-and-recover
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.
> [C++ exception for] IO failures […in idiomatic C++]
Oh? The standard library doesn't do this, right? What makes you say that's idiomatic C++?
Idiomatic Java, sure, but Java is just crazy about exceptions for everything.
> [C++ exceptions for] invalid states [… in idiomatic C++]
Idiomatic Go also calls for panic for the "can't happen" case.
> other errors that should be expected in the execution of a program all throw exceptions in idiomatic C++.
What makes you say this is idiomatic C++?
I've seen Go code that does it too, but that doesn't make it idiomatic Go.
Does the C++ standard library do this? The closest thing I can think of is std::bad_alloc on OOM, but then again Go panics then too.
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.
It costs binary size for unwinding tables, and in some cases it is an optimization barrier.
I do agree with the overall analysis, though.
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...).
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.
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.).
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).