Kudos to that author for the great, eye catching title and the in depth detail!
So horrifyingly true.
1: https://en.wikipedia.org/wiki/I_Have_No_Mouth,_and_I_Must_Sc...
CW though, it is pretty grim. Very early example of the "AI takes over the world, decides humans are redundant" trope though. (Maybe the first?)
You have "C with classes" that coexist with the "modern" way, full of smart pointers and functional programming. It is popular in embedded systems, video games, servers, and GUIs (mostly Qt). And if you look at the code, it is as if it was a different language, because the requirements are all very different. Embedded system need low level hardware access, video games are all about performance, servers want safety, and GUIs want flexibility.
There are less awful alternative to C++. For example C on one end of the spectrum and Rust on the other end. But none of them cover every C++ use case.
- There’s at least an ugly library to do it in C++
- There might be support baked directly into the language
- Or you could do it in Lisp, but that would be too easy
Well, it does unstructured imperative, structured imperative, and OOP imperative!
Except if you count template programming, because that one is pure functional, but only runs at compile time.
Literal lol... this is not an argument in favor of C++.
Actually, strike that: I'm not sure if it's true or not (though I suspect it is), but it doesn't actually matter. What I'm really getting at here is that there is nothing in Rust that behaves so confusingly or ambiguously as what's described in this article. If you're writing Rust, you'll never have to remember these sorts of rules and how they are going to be applied to your code.
I do agree that reading someone else's Rust can be a challenge, if they're using Rust's type system to its fullest, and you're (quite reasonably and understandably) not up to speed on the entirety of it. And that is a problem, agreed; though, at least, fortunately it's not a problem of ambiguity, but more of a learning-curve issue.
But I have never been writing Rust and have been confused about what the code I'm writing might do, never had to consult and puzzle out some obscure documentation in order to ensure that the code I was writing was going to do what I expected it to do. C++ falls so incredibly flat in this department, and that's why I avoid using it like the plague.
The fact that you can do almost anything IS pretty cool, but without having at least one C++ wizard at hand it can drive you nuts.
Almost every part of C++ creeps into almost every other part, and C was already complex enough... and let's just ignore that C++ is not completely compatible with C.
I think I found it via a stackexchange answer about how the "Wiring" language for Arduino sketches differs from regular c++. In Wiring, it's mostly things like no rtti, no virtual methods not resolvable at compile time, no exceptions, unsafe-math, permissive casting, pre-build system concatenates all .ino files into one .cpp file, very limited libraries, and some default includes.
If I was starting a new work project with a lot of junior team members, or if I was doing a web project, or a very simple script, fine I’ll use a different language. There can definitely be good reasons not to use C++. But I’m at the point in my expertise that I will default to C++ otherwise. I’m most productive where I am most familiar.
Not disagreeing that C++ is awful in a lot of ways and super difficult though. But I still weirdly like it, personally. I find it a fun challenge/puzzle to work with.
In any case, I think the primary goal of any programming language is to get out of your way and let you tackle more interesting problems related to the problem domain that led you to start writing a program in the first place.
You're basically saying the language gets in the way of solving your problem :)
It’s been a while now, and at least in my experience so far Go and Rusts choice of not having special constructors really simplifies a lot.
Is there anyone that’s had the experience of missing constructors once you swapped away from them?
Unless you want to `ptr::write` individual fields by hand into a `MaybeUninit`, which you can absolutely do mind but that… is not very ergonomic, and requires structs to be specifically opted into this.
Without guaranteed “placement new” that can mean that your 2MB object gets constructed on the stack and copied to the heap. And while Linux defaults to a 4MB stack, Windows defaults to 1MB and will crash your program. Or it might work if the compiler optimizes in your favor.
It's not something you encounter frequently, it can be worked around, and Rust will eventually solve it ergonomically without introducing constructor hell (probably with just a keyword). But finding the best language-level solution isn't straightforward (efforts to fix this for rust are ongoing for 9 years)
>Without guaranteed “placement new” that can mean that your 2MB object gets constructed on the stack and copied to the heap. And while Linux defaults to a 4MB stack, Windows defaults to 1MB and will crash your program. Or it might work if the compiler optimizes in your favor.
C gets a lot of hate, often for good reasons, but at least you know where your memory is coming from when you are allocating it yourself. If you're allocating a large heap-allocated object, you're grabbing the memory directly from the heap.
I have a hunch this is why there's no Semaphore implementation in the Rust standard library, though it could be due to fundamental inconsistencies in semaphore APIs across OSs as well ¯\_(ツ)_/¯
This take makes no sense. Think about it: you're saying that not having the compiler do any work for you "really simplifies things a lot". Cool, so you have to explicitly declare and define all constructors. That's ok. But think about it, doesn't C++ already offer you that option from the very start? I mean, you are talking about a feature in C++ that is not mandatory or required, and was added just to prevent those programmers who really really wanted to avoid writing boilerplate code to lean on the compiler in and only in very specific corner cases. If for any reason you want the compiler to do that work for you, you need to be mindful of the specific conditions where you can omit your own member functions. For the rest of the world, they can simply live a normal life and just add them.
How is this complicated?
Complaining that special member functions make obvious things less simple is like complaining that English is not simple jus because you can find complicated words in a dictionary. Yes, you can make it complicated if that's what you want, but there is nothing forcing you to overcomplicate things, is there?
All structs in Rust must be initialized using brace syntax, e.g. `Foo { bar: 1, baz: "" }`. This is commonly encapsulated into static functions (e.g. `Foo::new(1, "")`) that act similarly to constructors, but which are not special in any way compared to other functions. This avoids a lot of the strangeness in C++ that arises from constructors being "special" (can't be named, don't have a return type, use initializer list syntax which is not used anywhere else).
This combined with mandatory move semantics means you also don't have to worry about copy constructors or copy-assignment operators (you opt into copy semantics by deriving from Clone and explicitly calling `.clone()` to create a copy, or deriving from Copy for implicit copy-on-assign) or move constructors and move-assignment operators (all non-Copy assignments are moves by default).
It's actually rather refreshing, and I find myself writing a lot of my C++ code in imitation of the Rust style.
I don't think you managed to understand what I actually said, and consequently you wrote a whole wall of text that's not related to the point I made.
Here are some ways:
- As a junior programmer, it made the language harder to learn. Language complexity increases super-linearly as each new feature has rules of interaction with several existing features
- Although one eventually learns to avoid the anti-features, you cannot control the actions of others. These features meant to help save keystrokes are happily employed every day, producing hard to read code
Particularly when writing library code for other to use or when maintaining large codebases shared by hundreds of engineers, my experience is that complex features in the language end up used by junior engineers or require consideration in API design.
Can you elaborate on your opinion? I mean, I don't think that argument makes any sense. You're talking about an optional feature that, under very specific circumstances, you can get the compiler to fill in for you default implementations for factory methods/constructors.
As a junior developer, it should be very clear to you that if you want to call a function, including copy constructor or copy assignment operators, you need to define them first. Is that too much of a burden to place on a junior developer?
> Although one eventually learns to avoid the anti-features (...)
There are none of those, and obviously special member functions don't pose a problem to anyone.
> Particularly when writing library code for other to use or when maintaining large codebases shared by hundreds of engineers, my experience is that complex features in the language end up used by junior engineers or require consideration in API design.
I don't think you have a good grasp on the subject. I've worked on C++ libraries shared by hundreds of engineers, and API design was always from the start the primary concern. This is not a function of seniority: it's the very basics of writing modular code intended to be consumed by third parties.
Still, special member functions are the very least of anyone's concerns because anyone remotely competent in this domain knows very well that the public interface needs to be explicitly designed to support or reject specific types of uses, and the decision of whether a component could/should be copied/moved is tied to the component's architecture and semantics.
and from what I understand rust constructors are basically the same as java, no?
Rust has functions associated with types which are conventionally used like constructors, but critically the new objects must have all their fields provided all at once, so it is impossible to observe a partially initialised object.
[0] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...
https://github.com/titzer/virgil/blob/master/doc/tutorial/Cl...
[0] except in the more generalised haskell-ish sense that structs or enum variants can be constructed and some forms (“tuple structs” and “tuple variants”) will expose an actual function
I never thought that constructors were that burdensome and therefore do not understand the omission in other languages like Go and Rust that followed. Quite the opposite really -- knowing that a type always went through a predefined init was comforting to me when writing Java.
Eg:
- Java constructors can return the object before they complete construction, finishing at a later time; this is visible in concurrent code as partially constructed objects
- Java constructors can throw exceptions and return the partially constructed object at the same time, giving you references to broken invalid objects
- Just.. all the things about how calling super constructors and instance methods interleaved with field initialization works and the bazillion ordering rules around that
- Finalizers in general and finalizers on partially constructed objects specifically
I don't in any way claim it's on the same level as C++, but any time I see a Java constructor doing any method calls anymore - whether to instance methods or to super constructors - I know there are dragons
There are 3 which pertain to object initialization in Java.
1. super is initialized in it's entirety by an implicit or explicit call to `super()`
2. All instance initializers of the present class are invoked in textual order.
3. Constructor code following the `super()` call is executed.
The only awkward thing here is the position of #2 in between #1 and #3, whereas the text of a constructor body suggests that #1 and #3 are consecutive. It gets easier to remember when you recognize that, actually, there's a defect in the design of the Java syntax here. A constructor looks like a normal function whose first action must be a `super()` call. It's not. The `super()` call is it's own thing and shouldn't rightly live in the body of the constructor at all.
Edit: Tweaks for clarity.
Any language that has constructors has some complex rules to solve those things. And it's always good to check what they are when learning the language. Java has one of the simplest set of those rules that I know about.
Java constructors do not actually return the object. In Java code, it would appear to the caller as though the contructor returns the new instance, but that is not really the case. Instead, the new object is allocated and then the constructor is called on the object in (almost) the same manner as an instance method.
Additionally, Java constructors can only leak a partially initialized object if they store a `this` reference somewhere on the heap (for example, by spawning a thread with a reference to `this`). The assertion that this gives you a reference to a "broken invalid object" is only potentially correct from the perspective of invariants assumed by user-written code. It is perfectly valid and well-defined to the JVM.
> - Just.. all the things about how calling super constructors and instance methods interleaved with field initialization works and the bazillion ordering rules around that
This is a gross mischaracterization of the complexity. There is only a single rule that really matters, and that is "no references to `this` before a super constructor is called". Until very recently, there was also "no statements before a super constructor is called".
> - Finalizers in general and finalizers on partially constructed objects specifically
Finalizers are deprecated.
> Java constructors can throw exceptions and return the partially constructed object at the same time
Can you show some sample code to demonstrate this issue?https://codefibershq.com/blog/golang-why-nil-is-not-always-n...
That is why the epochs proposal was rejected.
Additionally, the main reason many of us, even C++ passionate users, reach out to C++ instead of something else, is backwards compatibility, and existing ecosystem.
When that is not required for the project at hand, we happily reach out to C#, D, Java, Go, Rust, Zig, Swift, Odin,.... instead.
No other language creates as many problems for programmers as C++.
but only if not knowing how the complicated parts work doesn't create any subtle issues and has reasonable compiler time errors and isn't fundamental needed to write any code
Because then you can use the language without needing to know how exactly that complexity works and if you get it wrong you get a reasonable compiler error. And then decide to either spend some time to learn what you didn't know or you just write the code differently. But in either case you don't have a situation with a unexpected runtime error which should be impossible and where you have no good way to know where to even start looking.
I like keeping the rules of the language simple enough that there is never any confusion.
That being said it’s way less complex than C++’s rules and that’s welcomef.
If you want to simplyfing by synthesising decades of accumulated knowledge into a coherent language, or to remove depreciated ideas (instead of the evolved spaghetti you get by decades of updating a language) then fine. If your approach to simplicity is to just not include the complexity, you will soon disciplinary that the complexity was there for a reason.
However I can't help but think that maybe I'm just so fortunate to be able to work in a nice code base optimized for developer productivity like this. C++ is really a nice language for experts.
to enforce all fields initialized.
If you care, the linter is there, so this is more of a skill issue.
The Default typeclass (or trait) as seen in Haskell and Rust, is a far better design, as this makes the feature opt-in for data types that truly support them.
Edit: this response is a bit dismissive but honestly my main beef with this article is that its conclusion is just straight up wrong. Do not write your own constructors, do follow the rule of 5/3/0, and if you find yourself needing to hold a const reference, you should look out for whether you’re passing in an rval temporary… none of this is really scary.
> In my humble opinion, here’s the key takeaway: just write your own fucking constructors! You see all that nonsense? Almost completely avoidable if you had just written your own fucking constructors. Don’t let the compiler figure it out for you. You’re the one in control here. Or is it that you think you’re being cute? You just added six instances of undefined behaviour to your company’s codebase, and now twenty Russian hackers are fighting to pwn your app first. Are you stupid? What’s the matter with you? What were you thinking? God.
The problem with C++ and the danger with an article like this is someone might actually follow this advice, instead of eg: the core guidelines.
Every other example is a violation of the core guidelines in some form or another. There is no other problem.
That's my take as well. The blogger clearly went way out of his way to find something to whine about while purposely ignoring a few of the most basic principles and guidelines.
In the meantime, everyone who ever went through a basic tutorial just casually steps over these artificial scenarios.
Your statement may be correct! But it’s certainly not common knowledge in my experience.
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...
std::reference_wrapper still can’t save you from yourself, but it’s better than violating the first link and ending up in this limbo that OP is talking about.
See also: https://youtu.be/YxSg_Gzm-VQ (about 3:30 in)
Nothing else.
T̶h̶a̶t̶ d̶o̶e̶s̶n̶'t̶ s̶e̶e̶m̶ c̶o̶r̶r̶e̶c̶t̶:̶ a̶ d̶e̶f̶a̶u̶l̶t̶e̶d̶ c̶o̶n̶s̶t̶r̶u̶c̶t̶o̶r̶ s̶t̶i̶l̶l̶ d̶e̶f̶a̶u̶l̶t̶-̶i̶n̶i̶t̶i̶a̶l̶i̶z̶e̶s̶ t̶h̶e̶ m̶e̶m̶b̶e̶r̶s̶, n̶o̶t̶ v̶a̶l̶u̶e̶ i̶n̶i̶t̶i̶a̶l̶i̶z̶e̶. I̶ d̶o̶n̶'t̶ t̶h̶i̶n̶k̶ t̶h̶e̶r̶e̶ i̶s̶ a̶n̶y̶ d̶i̶f̶f̶e̶r̶e̶n̶c̶e̶ b̶e̶t̶w̶e̶e̶n̶ d̶e̶f̶a̶u̶l̶t̶i̶n̶g̶ i̶n̶l̶i̶n̶e̶ a̶n̶d̶ o̶u̶t̶ o̶f̶ l̶i̶n̶e̶. G̶C̶C̶ s̶e̶e̶m̶s̶ t̶o̶ a̶g̶r̶e̶e̶:̶ h̶t̶t̶p̶s̶:̶//g̶c̶c̶.g̶o̶d̶b̶o̶l̶t̶.o̶r̶g̶/z̶/r̶4̶r̶e̶5̶T̶E̶5̶a̶
edit: I missed that the author is actually value-initializing x!!! The result definitely violates expectations!
Generally, the details of the rules are arcane and sometimes have non-sensical dark corners having been extended and patched up for the last 40 years. But 99.9%[1] of the time you get what you expect.
I big improvement would be making default initialization explicit, and otherwise always value initialize. Explicit value initialization is so common that the very rare times I want default initialization (to avoid expensively zeroing large arrays) I need to write a fat comment. Writing "std::array<int, 100> = void;" (or whatever the syntax would be) would be much better.
[1] I had an extra 9 here... I hedged.
Actually initializing your instances, which is what's expected of every single instantiation, is enough to not experience any problem. This also means that if you want to call a constructor, you need to define it.
This is a case of people trying to be too clever for their own sake, and complaining that that's too much cleverness for them to handle.
> So, here’s the glue between list-initialization and aggregate initialization: if list-initialization is performed on an aggregate, aggregate initialization is performed unless the list has only one argument, of type T or of type derived from T, in which case it performs direct-initialization (or copy-initialization).
The word “unless” is even bold.
We have fancy syntax:
T t{v0};
And we also have: T t{v0, v1};
And so on. But the one-element case does not reliably work like the 2+-element case. And this is in a language that increasingly works toward making it straightforward to create a struct from a parameter pack and has support for variable length array-ish things that one can initialize like this. And the types can, of course, be templated.So you can write your own constructors, and you can initialize a tuple or array with only one element supplied, and you might trip over the wrong constructor being invoked in special cases.
I remember discovering this when C++11 initializer lists were brand new and thinking it was nuts.
I'm hoping something like Herb's C++ syntax 2 will make the language useable for mortals like me.
I think you're whining about something that doesn't pose any problem to anyone with any passing experience in software development.
The examples in the blog post boil down to far-fetched cases devised to trigger corner-cases of a feature where a programming language in exceptional cases auto-generates specific functions when programmers somehow explicitly decided not to do it themselves. The blogger then proceeds to explore edge conditions that lead these exceptional cases to either be enabled or disabled.
In the meantime, be mindful of the fact that this is about a language with a clear design goal of paying only for what you use,and also the widely established rule of 3/rule of 5, which is C++101 and states that when anyone defines one of these special member functions, they should define them all. Why? Because it's C++101 that these special member functions are only generated by the compiler in specific corner cases, and given their exceptional nature the compiler will not generate them automatically if any of the requirements is not met.
Therefore, any programmer who goes through the faintest introduction knows that they have to set the constructors they will use. Is this outlandish?
Also, does it catches anyone by surprise that you need to initialize instances when you instantiate them? Is this too much of a gotcha to justify being called "horrific"?
I think people like you just feel the need to have something to complain about. In the meantime, everyone in the real world happily does real work with them without any fuss.
Thanks for the ad hominem
That can't be right ... is it? Things cannot be initialized twice. Isn't it more like "Otherwise, recurse the value-initialization over the bases and members". Then, those that are not classes or arrays get zero-initialized.
It doesn't mean that the lifetime of the object starts twice, or anything weird like that.
You can only initialize once. After it's been initialized you're just assigning values, and that's not what happens during initialization. It's either a misunderstanding on behalf of the author or the words as written are not conveying the correct idea.So their claim that "T t;" will "do nothing" is incorrect.
class T
{
public:
T(int);
};
T t;
Will fail.> Primarily, there are two kinds of initialization of concern: default-initialization and value-initialization. The rules given in the standard look roughly like this:
> * For any type T, T t; performs default-initialization on t as follows: ...
As GP mentions, the article's descriptions of default and value initialization are both incorrect for classes that do not have default constructors, as that code will simply not compile.
Such as all the constructors that are being added, implicit copy constructor and all the other surprises?
https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...
T::T() = default;
> You’d expect the printed result to be 0, right? You poor thing. Alas—it will be garbage. Some things can never be perfect, it seems. Here’s a relevant excerpt from our description of value-initialization:Link: https://consteval.ca/2024/07/03/initialization/#:~:text=You%...
That actually isn't that weird, because it would allow any consumer of your library to change how your library behaves.
For good or for ill, I don't really trust anything that long to be something the average human can wrestle down. And when you mix in undefined behavior and the fact it's still used on safety-critical systems... It's a bit horrifying really.
I wonder how much shorter the spec for a radically simplified version of C++ -- say, one that zero-initializes everything, and has no default constructors ("not writing a constructor raises an error" or something) -- would be? 5% 50? 95...?
It's 15 years out of date now, but also timeless since C++ rarely/never removes old features or behaviours.
"Call J. G. Wentworth, 877 Cash Now!"