After almost 20 years of experience with C++, there are still some gnarly details I wouldn't have imagined. What a God awful language!
Kudos to that author for the great, eye catching title and the in depth detail!
After almost 20 years of experience with C++, there are still some gnarly details I wouldn't have imagined. What a God awful language!
Kudos to that author for the great, eye catching title and the in depth detail!
Man I get vertigo reading this. Reminds me of trying to understand Java constructors and object initialisation.
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?
dude, java constructor are easy... that C++ stuff is really black magic
and from what I understand rust constructors are basically the same as java, no?
I think if you think constructors in Java are easy, you are much, much smarter than I am or have missed some really, really subtle footguns.
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
I think you’re exaggerating the complexity here. There are corner cases yes, but the compiler will warn you about them.
> 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?Those are the normal issues inherent to constructors as a concept (except for the finalizer one).
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.
bazillion ordering rules
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.
- 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
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.
Rust does not have constructors at all[0], it uses factory functions (conventionally named `new_somethignsomething`) but those are not special to the language.
[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've often longed for first class constructors in Go and Rust. It was more of a problem for me with Go because you can omit a struct field when building a value, something you can't do in Rust unless it has an explicit Default impl and even then you have to explicitly add ..Default::defualt() when you're building the value.
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.
I think people don’t like constructors because of the potential side effects of something happening in constructors, especially if the constructor is big or doesn’t finish properly.
Inside a constructor you can access a partially initialised "this" value, and even call methods on it, which leads to rules like: "Do not call overridable methods in constructors"[0], as they can lead to surprising, non-local, bugs.
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...
You can most likely use session types to soundly observe a partially initialized MaybeUninit<MyObject> in Rust. The proper use of session types could ensure that the object is only assumed to be initialized after every field of it has been written to, and that no uninitialized fields are ever accessed in an unsound way. The issue though is that this is not automated in any way, it requires you to write custom code for each case of partial initialization you might be dealing with.
Virgil solved this a little differently. The initialization expressions for fields (outside of constructors) as well as implicit assignment of constructor parameters to fields happens before super constructor calls. Such initialization expressions cannot reference "this"--"this" is only available in _constructor bodies_. Initializing fields before calling super and then the chaining of super calls guarantees the whole chain of super constructor calls will finish before entering the body of a constructor, and all fields will be initialized. Thus by construction, virtual methods invoked on "this" won't see uninitialized fields.
https://github.com/titzer/virgil/blob/master/doc/tutorial/Cl...
Rust doesn't have constructors. By convention, a static method called new returns a struct - no magic.
There are a few somewhat esoteric cases where constructors working in-place allow magic which can be hard to replicate otherwise e.g. Rust is still missing guaranteed “placement new” type behaviour.
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.
Which can be an issue if you want to initialize a 2MB large heap-allocated object (e.g. heap-allocating a large nested struct or a big array).
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)
Which can be an issue if you want to initialize a 2MB large heap-allocated object (e.g. heap-allocating a large nested struct or a big array).
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.
Memory allocation is one of the areas where currently C/C++ has or had genuine advantages over Rust. Custom allocators took Rust years, and giving standard library constructs like a Vector a custom allocator that isn't the global allocator is still experimental (=opt-in nightly-only). Similarly while Rust gives you good control over where the data ends up being stored, there is no way to make sure it isn't also put on the stack during function execution. One of the implicit assumptions underlying the language seems to be that the stack is cheap and effectively infinite while the heap is expensive. So you have a lot of control over what touches the heap, but less control over what touches the stack.
Those are temporary pains that have remedies in the works. Rust is a fairly young language, and a lot of good-enough solutions get thrown out before ever getting beyond the experimental stage. But if you are writing software today then needing absolute control over where exactly your data touches is a good reason to prefer C/C++ today. Not that that's a very common need.
I'm not persuaded that scribbling on a Box<MaybeUninit<T>> until it's initialised is less ergonomic than the C. Which isn't to say it's a desirable end state, I just don't see C as a more ergonomic alternative even for this application.
It can also be an issue if you want to wrap any API that requires fixed memory locations for objects (such as POSIX semaphores). It's UB to call a POSIX semaphore from any other memory location than where it was initialized, so making a `Semaphore::new()` API is just asking for trouble. You can deal with it by `Box`ing the semaphore, but then you can't construct the semaphore in a shared memory segment (one of the stronger use cases for process-shared semaphores).
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 ¯\_(ツ)_/¯
No, Rust doesn't have semaphores in the stdlib[0] because it was not clear what precise semantics should be supported, or what purpose they would serve since by definition they can't mitigate exclusive and thus write access to a resource and mitigating access to code isn't much of a rust convention. And nobody has really championed their addition since.
Furthermore, they still present a fair amount of design challenges in the specific context of Rust: https://neosmart.net/blog/implementing-truly-safe-semaphores...
[0] technically they were there, added in 0.4, never stabilised, deprecated in 1.7, and removed in 1.8
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.
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?
You're mistaken. Rust does not require you to define all constructors. Rust does not have constructors.
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.
You're mistaken. Rust does not require you to define all constructors. Rust does not have constructors.
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.
Your post starts with the flawed assumption that you have to define constructors in Rust, and then your own wall of text (ironically longer than mine) about avoiding boilerplate which doesn't apply to Rust. I'm not sure you understood my point.
Just to further illustrate what I'm saying, are you really trying to say that
``` //explicitly annotating this struct is default initializable and copyable #[derive(Default, Copy, Clone)] struct Foo { ... } ```
is actually worse than
``` struct Foo {...}; // rule of zero, copy/move/default are defined/deleted based arcane rules predicated on the contents of Foo ```
I mean go's zero initialization requires a bit of language lawyering sometimes too.
https://codefibershq.com/blog/golang-why-nil-is-not-always-n...
A language shouldn't be this complicated. This is dangerous and impossible for teams full of juniors and busy people with deadlines. We're only human.
I believe those teams just use constructors; this is a corner case, not SOP.
C++ should start pulling things out of the language with new editions. It would improve quality of life dramatically.
Rust style editions don't work with binary libraries, across compilers, or template code across editions, with semantic differences.
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.
When that is not required for the project at hand, we happily reach out to C#, D, Java, Go, Rust, Zig, Swift, Odin,.... instead.
Which is all well and good for us, the application developers. But if C++ wants to exist in the future as a thriving language (as opposed to moving in to the nursing home with Cobol and Fortran), then it needs to come up with some solution to remove cruft.
Until those languages decide to be fully bootstraped, alongside Khronos and OpenGroup being welcoming to them for newer standards, C++ won't go away.
It has a solution: obsoleting features and then removing them. For examples, see
https://en.wikipedia.org/wiki/C%2B%2B17#Removed_features
https://en.wikipedia.org/wiki/C%2B%2B20#Removed_and_deprecat...
https://en.wikipedia.org/wiki/C%2B%2B23#Removed_features_and...
Part of their ‘problem’ is that they have lots and lots of users with long-living code bases. That means that, if they move fast and break things, their users won’t move to newer versions of the language.
Another part is that they want to be able to generate the fastest code possible. That leads to such things as having all kinds of constructors (‘normal’ ones, copy constructors, move constructors), and giving developers the ability to tweak them for maximum performance.
In addition, a lot of this was invented after the language was in use for decades. I think that makes the constructor story more complicated than needed.
If Rust style editions can work across modules, why couldn't they work across binary libraries and so forth? The whole point is to allow the language to progress while maintaining backward compatability.
they don't work with a backward compatible application binary interface
or more specifically they only work with ABI stability if they ABI doesn't change between epochs
which isn't a issue for Rust because:
- it is cleanly modularized
- it build a whole module "at once"
- it doesn't have a "stable" ABI (outside of "extern/repr C" parts which don't contain non reprC parts rust doesn't even guarantee ABI compatibility between two builds in exactly the same context*(1))
- tends to build everything from source (with caching)
- a lot of internees are intentionally kept "unstable" so that they can change at any time
on the other side due to how C/C++ build things, doesn't have clean module isolation, how it chooses build units, how all of that is combined, how it's normal to include binaries not build by your project (or even you), how such binaries contain metadata (or don't) and how too much tooling relies on this in ways which make changes hard, how it doesn't have build-in package management, how it you specify compiler options and how compiler defaults are handled etc. come together to make that impossible
in a certain way how you specify that you use C++11,17 etc. is the closest C++ can get to rust editions
like initially it might seem easy to introduce syntax braking changes (which most rust edition changes boil down to) but then you realize that build units using other editions have to be able to read the header file and the header file e.g. in context of templates can contains any kind of code and that header includes aren't that much different too copy pasting in the header and that you don't have a standard package manager which can trace which edition a header has and endless different build systems and you kinda give up
purely technically it _is fully possible to have rust like editions in C++_ but practically/organizationally in context of e.g. backward compatibility with build systems it's just way to disruptive to be practical
don't work with binary libraries,
None of the edition changes that Rust has made have any effect on the ABI. It also has no stable Rust ABI, so there wasn't an effort to formalize that, but 1) ABIs should always be versioned (to avoid getting stuck with a bad ABI) and 2) you can use editions for other kinds of change in the meantime.
across compilers,
This is almost tautological. Yes, having two C++ compilers agree to their support of editions is the same as them agreeing to their support of concepts. I don't see how this is a critique of the strategy.
template code across editions, with semantic differences.
Rust defines editions at the compilation unit level: the crate. But it has macros (which are akin to templates) and editions are handled at that boundary (you can have code in one edition invoke macros in another) because the compiler tracks editions at the token level (the information is attached to their Span). There's no reason editions in C++ can't work with templates. You would have to specify the edition of the template, and given C++ semantics you might have to have an opt-in scope to say "use this edition here, override the rest of the file", but it would be possible.
Rust editions are very conservative, expect everything to be built from source, with the same compiler, and don't touch semantic changes across versions, mostly grammar changes.
Example, there is no story for scenario, where a callback defined in one version, is used in another crate version, calling into code, using yet another version, while passing a closure with a specific type with semantic changes across all versions.
I am not yet convinced they will scale at the size of industry use cases for C and C++, with a plethora of compilers, and mixing several editions on a 30 year old codebase.
How about (C++)-- ?
There was a C--, and it was an Assembly macro C like syntax based compiler.
https://en.wikipedia.org/wiki/C--
The GNU gcc/g++ was far more important to standardization than most people like to admit.
Have a great day, =)
True, however in practice this is rarely an issue. You usually only use a tiny subset of construction rules. And if you ever make a mistake, they are easily caught by static analysis tools.
It’s quite a big issue. It’s actually a bit worse than the article makes out if you throw static objects into the mix and get race conditions where you don’t know which objects get constructed first. C++ has to be approached with caution even by experienced devs.
I agree with the parent - global initialisation order is a once-bitten-never-again, and the reality is that working in most codebases doesn't require understanding all of these rules - knowing the quirks is usually only required by one or two people and the rest can work with what they've got.
experienced (and even the not so much) devs know the perils of static initialization and avoid it.
that is a novice issue, it is easily avoided
it being this complicated can be fine (if there isn't too much of it)
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.
its funny how people are down voting this when it's exactly the approach rust uses
I've come to the conclusion that most if not all the complexity is largely to justify the existence of those who work on "moving the language forward".
Working as a junior in a C++ codebase is great for my career because the skill floor is so high. Because it's so difficult to do anything there's a lot of room to distinguish myself.
No other language creates as many problems for programmers as C++.
My guess is all these details are necessary to provide C++ "strong exception guarantee" against partially constructed objects. Perhaps if your member objects can throw exceptions, some of these pedantic initialization rules can come to the rescue and allow, say, a library implementor to limit initialization code to places where exceptions can be handled.
They can't pull the rug out now, but I highly recommend making your own clang-tidies to flag confusing constructs (like defaulted out-of-line constructors) and preventing them from being committed.
I'm so glad I use Go more than C++ these days. In Go, all values are always zero-initialized if you don't explicitly assign a value. If you need a constructor, you write a regular function that returns an explicitly assigned object.
I like keeping the rules of the language simple enough that there is never any confusion.
Personally I’m not a fan of Go’s default zero-initialisation. I’ve seen many bugs caused by adding a new field, forgetting to update constructors to intialise these fields to “non-zero” values which caused bugs. I prefer Rust’s approach where one has to be explicit.
That being said it’s way less complex than C++’s rules and that’s welcomef.
The problem you are describing in Go is rarely a problem in C++. In my experience, a mature code base rarely has things with default constructors, so adding a new field will cause the compiler to complain there's no default constructor for what you added, therefore avoiding this bug. Primitive types like `int` usually have a wrapper around them to clarify what kind of integers, and same with standard library containers like vector.
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.
Why would you have a wrapper around every primitive/standard library type?
Type safety.
Compare `int albumId, songId;` versus `AlbumId albumId; SongId songId;`. The former two variables can be assigned to each other causing potential bug and confusion. The latter two will not. Once you have a basic wrapper for integers, further wrappers are just a one-liner so why not. And also in practice making the type more meaningful leads you to shorter variable names because the information is already expressed in types.
Wouldn’t it just be considered bad practice to add a field and not initialize it? That feels strongly like something a code review is intended to catch.
It’s easy to miss this in large codebases. Having to check every single struct initalisation whenever a field is added is not practical. Some folks have mentioned that linters exist to catch implicit initialisation but I would argue this shouldn’t require a 3rd party project which is completely opt-in to install and run.
All bugs are considered bad practice, yet they keep happening :P
I spent a year and a half writing go code, and I found that it promised simplicity but there an endless number of these kinds of issues where it boils down to "well don't make that mistake".
It turns out that a lot of the complexity of modern programming languages come from the language designers trying to make misaked harder.
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.
Yea this can be problematic if you don’t have sum types, it’s hard to enforce correct typing while also having correct default / uninitialized values.
You can always use exhaustruct https://github.com/GaijinEntertainment/go-exhaustruct
to enforce all fields initialized.
If you care, the linter is there, so this is more of a skill issue.
FWIW there is a linter that enforces explicit struct field initialization.
Haven't written Go in a long time, but I do remember being bit by this.
The downside is now all types must dogmatically have a nullary constructor.
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.
Don't even need to go that far. In C++ it is common to delete the default constructor anyways. So that achieves the opt-in.
Having to delete the default constructor means it’s opt-out, not opt-in
I don't think you really know what a real-life C++ code base looks like. Deleting the default constructor often happens implicitly with no syntax needed. This often happens when you define a custom constructor or when its member doesn't have a default constructor.
Go makes a completely reasonable tradeoff, giving away performance to gain ease of use. C++ also makes a tradeoff that seems reasonable or necessary to its users, that it makes it possible to not be forced to write to the same class member twice, in exchange for a more complex language. Any language that attempts to offer this property unavoidably adopts the complexity as well. See, for example, Java.
The whole thing is wrong. Don’t put const references in your structs. Use std::reference_wrapper if you must.
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.
I’ve never used std::reference_wrapper in my life. Nor have I seen it used in any of the numerous C++ code bases I’ve worked in. Although I’m sure it’s used in deep, gnarly template BS.
Your statement may be correct! But it’s certainly not common knowledge in my experience.
My stance is just based on cpp core guidelines:
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)
That link says nothing about std::reference_wrapper ?
I’m just saying, nothing up my sleeve, no arcane templatelord bullshit, this is isocpp: don’t use references in your structs because it’s subtly broken. It doesn’t mention reference wrappers but that’s the escape hatch.
I use them now and then -- and I never touch templates. It's just a reference you can reassign. Or, in other words, truly a pointer that can't be null. :D (And that you can actually use in collections that require reassignable types.)
If you want to use assignment operators with references that is what it does.
Nothing else.
Rule of 5/3/0 is about destructors, move and copy constructors. Apart from the last example, the article mainly talks about quirks with the standard constructor, if you can call it that.
main takeaway of the article according to the author, quoting:
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.
Don't know why I got sniped into this on a friday night, but here's clang-tidy with only cppcoreguideline checks enabled, against every example:
First example: https://godbolt.org/z/898rorEqG
Second example: https://godbolt.org/z/K6aceesG9
Third example: https://godbolt.org/z/KcbqzMzdK
and I can't get the fourth example to compile without fixing the problems.
Edit: this response is a bit dismissive but honestly my main beef with this article is that its conclusion is just straight up wrong.
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.
The article is just plain wrong about classes: if you have declared any constructor, then the language will not provide a default constructor and default-initialization will fail with a compiler diagnostic.
So their claim that "T t;" will "do nothing" is incorrect.
class T
{
public:
T(int);
};
T t;
Will fail.I think I missed where in the article they did a `T t;`... Doesn't seem to show up with an eyeball scan?
In the first few paragraphs:
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.
But... all of the classes in the article -do- have default constructors. And all of the examples in the article do compile. So I'm confused at what point you guys are making.
Quoting the full section on `T t;`:
- If T is a class type and there is a default constructor, run it.
- If T is an array type, default-initialize each element.
- Otherwise, do nothing.
That decision tree should read: "If T is a class type: it will invoke the default constructor. It is a compile-time error to write this if T does not have a default constructor." Not "if there is a default constructor, run it; otherwise, fall back to doing nothing."
The "do nothing" applies to scalar types such as ints, and indirectly to scalar member variables that aren't explicitly initialized one way or another. Not to classes that have deleted the default constructor.
Yes, this. It appears these commenters are claiming that the author said something they did not say. Rather, there is a corner case that the author has not spoken about in this article.
... Which is not surprising, given that we're talking about a language with a spec that dwarfs most works of human literature for sheer mass.
I'm confused at how your comment is relevant to this article. Here you've written `T(int);`, i.e. a constructor with an argument. None of the classes declared in the article have a constructor that takes any arguments. Nor does the text of the article seem to make any statement which contradicts what you've asserted here. And all of the examples in the article compile successfully.
[...] The printed result would be 0. This is because we value-initialize t and, since T has a non-user-provided default constructor, the object is zero-initialized (hence t.x is zero-initialized) then default-initialized (calling the implicitly-defined default constructor, which does nothing).
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.
Once every thousand lines you don’t get what you expect? Rip
Once every 1000 initializations. But hey, I would sign up for only one bug every 1000 lines.
One of this particular class!
I big improvement would be making default initialization explicit
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.
I’m surprised there were no snarky comments about:
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.
Initializer lists is irreverent, nobody uses it anyway. Other than a few standard containers that use it, you can completely ignore the silly thing.
I've definitely seen initializer lists recommended as best-practice in safety-critical code.
As is the nature of bad design, “nobody uses it other than some people sometimes” is a silly sentiment and indicative of a problem.
That's the biggest problem with them. Something isn't as dangerous when it's in your face and you need to confront it on a regular basis. What's dangerous are the things that rarely need to think about, except for that very rare moment when it bites you in the ass precisely because it's not on your mind.
Otherwise, zero-initialize and then default-initialize.
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.
I think it would be perfectly legal to zero-initialize the entire thing and then default-initialize, because initialization assumes the value is undefined.
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.
Both "zero-initialize" and "default-initialize" are terms that have precise definitions. In this context if you substitute the definitions it just means first zero-initializing the non-static data members (and zeroing the padding), then calling the default constructor.
It doesn't mean that the lifetime of the object starts twice, or anything weird like that.
Is there a C++ tool that adds/shows all the implicit stuff that happens behind the scenes?
Such as all the constructors that are being added, implicit copy constructor and all the other surprises?
Best you're gonna get is a combination of godbolt and cppinsights.
cppinsights looks like what I was looking for, there's even a vscode extension thanks
What an beautiful blog theme, obviously inspired by the DEC-era computers but also clean and minimal. Refreshing!
Ditto
I like how the rules to the right of the headings react to the page width and how many lines are used.
I Have No Mouth, and I Must Scream (1967)
https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...
Came for this. I once talked with Harlan Ellison. He called my house when I was 17. At 2am. This was in the days of sharing a phone via a 30ft cord and taking it into your bedroom and closing the door. As teenagers would do. Martin H Greenberg and his family were staying at our house. He was calling for Martin. I was big into SciFi and had read a few of his books. The conversation was odd. I said Martin was asleep and asked who he was. Everything after hearing his name was sort of a blur. Yes, I did wake Martin and hand him the phone. It was hard to sleep after that.
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.
The alternative not making sense doesn't automatically make this solution make sense :( It just highlights how many corners C++ has backed its design into.
Horrific. One of the things that scares me about C++. A real shame because it has some cool features and some brilliant minds working on it.
I'm hoping something like Herb's C++ syntax 2 will make the language useable for mortals like me.
Horrific.
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.
Upvote for the title.
this has got to be one of my favorite blog names i've seen on this site
Great link, I'm going to add this to my list of favorite interview questions. (^_-)
Realizing the spec for C++23 is about two-and-some-change-times the length of the King James Bible has really reframed my thinking on this language.
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.
As an aside, I see that DEC front panel you've got there in your blog header.
I agree there is a lot of complexity in C++ in the year 2024, however I feel that much of the appearance of complexity around initialization is due to the pedantic, dogmatic use of the word "default" by the committee to mean "not".
this title makes me want to shout
"Call J. G. Wentworth, 877 Cash Now!"
Show me someone who understands the rules, and I'll show you a compiler engineer... who probably doesn't understand the rules.
For more C++ wackiness, I recommend the C++ FQA: https://yosefk.com/c++fqa/
It's 15 years out of date now, but also timeless since C++ rarely/never removes old features or behaviours.
This is the best title, OP.
I've been working with C++ at my job for 2.5 years now and I've already come to this conclusion. Wouldn't wanna use it if there is any other way.
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.
Just another person’s opinion: I’ve been using C++ for my entire career, and to be honest, if I’m starting a new solo project, I reach for it unless there is some major technical reason not to. Yes, it can be messy. Yes, there are footguns. But as a developer, you have the power to keep it clean and not shoot the footguns, so I’m still ok with the language.
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.
"you have the power to keep it clean and not shoot the footguns". Really? Do you think footguns are intentionally shot?
What even is a footgun supposed to be? The analogy doesn’t really make sense, in that… I mean the first thing anybody learns about guns is that they are “always loaded” (even when you know they aren’t) and you only point them at things you want shot.
Is a footgun a gun that only aims at feet? Because that seems like a dumb thing to invent in the first place. Or is it a gun that happens a to be aiming at feet? That seems like something that could only exist by user error.
I think “enough rope to hang yourself” is a more accurate description of almost every programming languages, since rope is at least intended to be useful (although it is a bit more morbid of an analogy).
Imagine that you had a gun and one of the features of the gun was that if you had sunglasses on and something in your left pocket, holstering the gun would cause it to immediately fire. You could argue that the gun shouldn’t behave this way, but it’s also possible that others are dependent on this behavior and you can’t remove it.
This is a footgun - the way to avoid the holster firing is to simply not wear sunglasses, or keep something in your left pocket, and then it would never fire. But the problem is that both of those things are extremely common (for good reason). It’s a poorly thought out feature because it has severe consequences (potentially shooting your foot) for extremely common situations (wearing sunglasses and using your left pocket).
I basically don’t agree that anybody could depend on this holstering-causes-it-to-fire behavior. Or at least, their use-case requires design compromises that are so unthinkably ridiculous as to make the gun they want something that no reasonable person without that use-case would use.
It is possible that the entire field of programming is full of ridiculous people. But it seems more likely that C++ is like a gun with no safety, or something along those lines.
Just to carry onwards with the use/mention distinction we're aggressively erasing here, you seem to question whether actual, real-life footguns exist.
They do! Here's a reference. https://en.wikipedia.org/wiki/Slamfire
Another common way to shoot yourself in the foot is a gun which will go off if you drop it. An example of a gun where early models were especially susceptible is the Lanchester: https://en.wikipedia.org/wiki/Lanchester_submachine_gun
It's an idiom. It's not supposed to be entirely logically consistent. It means what it means because people have decided that it means what it means. Your objections aren't really relevant.
"Footgun" is I think a fairly recent addition to the English lexicon, but it's based on the centuries-old "to shoot yourself in the foot". It seems silly to argue with centuries of English idiomatic usage; no one, by definition, is going to win an argument against that.
A lot of the footguns come from compiler authors wanting to make things UB because it allows them to perform certain optimizations, but then you end up with a lot of things that are formally UB even though in practice they usually do the intuitively expected thing. But then, because the widely done thing is actually UB, the compiler is allowed to do something counterintuitive which causes your program to blow up.
An obvious example is omitting NULL pointer checks. Passing a NULL pointer to certain system library functions is UB even if it would ordinarily be expected to be reasonable, e.g. memset(NULL, 0, 0), so some compilers will see that you passed a pointer to memset, and passing a NULL pointer to memset is UB, therefore it can omit a subsequent NULL pointer check guarding a call to something else when the something else isn't going to behave reasonably given a NULL pointer.
This is an insane footgun, but it also allows the compiler to omit a runtime NULL pointer check, which makes the program faster, so people who care most about performance lobby to keep it.
Yeah I don't think that's a good analogy. Instead, you have guns that don't let you point at your feet. So you can never shoot yourself there. However, if you ever need to shoot straight down for a legitimate reason, you're out of luck. In C++, you can shoot everywhere without restrictions and sometimes that means shooting yourself in the foot or the head.
At this point a footgun can stand alone in this industry as a term with its own meaning outside of analogy.
It is any trap in something technical that is likely to cause problems from perceived normal use.
Compare to related terms: "Pit of failure", "Turing tarpit", and "Pit of success".
I've long been partial to this formulation:
Some of us learn to lean to the side right before pulling the trigger...
http://james-iry.blogspot.com/2009/05/brief-incomplete-and-m...
"Footgun" comes from the English idiom "to shoot yourself in the foot", which means "to act against your own interests" (usually accidentally). (Consider similar idioms, like "to dig your own grave".)
I think you're being a bit too literal. It's not an analogy at all, and this has nothing to do with firearms best practices. If we were to define a footgun as "a gun that is only capable of shooting you in the foot" (or perhaps more broadly usefully, "a gun that in theory can be useful, but it is nearly impossibly difficult to make it do anything other than shoot you in the foot"), then the entire point of using the term is to describe something that has no useful, logical purpose, and is unsafe to use, even as designed.
Being "given enough rope to hang yourself" is indeed another good idiom to use for things like this, but the implication is different, I think: when you're given enough rope to hang yourself, the outcome is still very much in your hands. You can intentionally or unintentionally use that rope to hang yourself, or you can be reasonably expected to use that rope in another way that would turn out to be safe or useful.
"Footgun", by contrast, is used to describe something that has no (or negligible) safe uses. Maybe the original intent behind the design of what's being described that way was to have safe uses, but ultimately those safe uses never really panned out, or were so dwarfed by the unsafe uses that the safe use isn't worth the thing existing in the first place. But, unfortunately, there are some people -- maybe only 0.01% of people -- who are able use it safely, and critically depend on that safe use, so we can't completely destroy all these footguns and save everyone else from the error of their ways. And unfortunately most everyone else sees these 0.01% of uses, and believes they are so useful, so efficient, so brilliant, they want to try it too... but in their hubris they end up shooting themselves in the foot, like most others before them.
There are more an less risky behaviors. This is really well explored space in C++. Just using value semantics, shared_ptr, and RAII instead of naked news and reckless mallocs would improve several "old" codebase I have worked in. Maybe people shouldn't be reaching for const_cast so often, and similar. In some new languages some corner case may be unexplored.
If you are fortunate enough for your domain to have good IO libraries then there is a chance you can do everything the "Modern" way avoid a lot of the headache and avoid most of the footguns entirely. That maturity and omni-pattern availability is potent, but all that power does come with the possibility of mistakes or surprises.
Compare to newer languages where we don't know what might break or might need to do something the language omits as part of its paradigm. I have done a ton of Ruby projects and about half the time we need more performance so I need to bust out C to rewrite a hotspot in a performance sensitive way. Or sometimes you just really want a loop not a functional stream enumerator like is the default in Ruby. For a new language, Theo tried the 1 billion row challenge in Gleam and the underlying file IO was so slow the language implementers had to step in.
This is an engineering and a business choice. There are reasons to avoid C++ and footguns, like any risk, are among them. These aren't risks without mitigation, but that mitigation has a cost. Just like newer languages have reasons not to use them. A team needs to pick the tools with risks and other cons they can handle and the pros that help them solve their problem.
The problem is that your definition of risk may not be the same as others', and so there isn't always agreement on what is ok and not ok to do. And regardless, humans are notoriously bad at risk assessment.
Right, and all that is exactly the point: all of that stuff is in wide use out there, and I suspect not just in "old" code bases. So there's still not consensus on what's safe to use and what's too risky.
And regardless, I have enough to think about when I'm building something. Remembering the rules of what language features and APIs I should and shouldn't use is added noise that I don't want. Having to make quick risk assessments about particular constructs is not something I want to be doing. I'd rather just write in a safer language, and the compiler will error out if I do something that would otherwise be too risky. And as a bonus, other people are making those risk assessments up-front for me, people in a much better position than I am to do so in the first place, people who understand the consequences and trade offs better than I do.
I really like this value proposition: "if the compiler successfully compiles the code, there will be no buffer overruns or use-after-free bugs in it" (certainly there's the possibility of compiler bugs, but that's the only vector for failures here). For C++, at best, we can only say, "if you use only a particular subset of the language and standard library that the compiler will not define or enforce for you (find a third party definition or define it yourself, and then be very very careful when coding that you adhere to it, without anyone or anything checking your work), then you probably won't have any buffer overruns or use-after-free bugs." To me, that's almost worse than useless; even if I find a C++-subset definition that I think is reasonable, I'm still not really protected, because I still have to perfectly adhere to it. And even if I do, I'm still at the mercy of that subset definition being "correct".
This is “I use the language I always use because I always use it”, and not actually a praise or C++ specifically.
Presumably "entire career" means some amount of exposure to other things.
In my 21 years coding professionally, I will settle on C++ or Ruby for most problems depending on context. Ruby for quick, dirty, and "now!", while I use C++ for Long lasting, performance, strongly typed, and compatible things. Those aren't perfect categories and there are reasons to pick other tools, but Choosing C++ after a long career does mean something more than "I am comfortable with this".
Does it?
With all due respect to your expertise, the whole idea of a footgun is that it tends to go off accidentally. The more footguns a language contains, the more likely you are of accidentally firing one.
I think what he's saying is that C++ users don't need to go to the Footgun Outlet Mall and wantonly brandish each of their credit cards. You can leave the subtle parts of the language on the shelf, in many cases.
I don’t think I’ve ever gotten paid for a line of c++ but Google has a “style guide” for internal c++ code that omits something like 3/4 of the language, and people seemed pretty happy with it overall. Maybe not “happy” but “grudgingly accepting because it beats the Wild West alternative”.
is google's "internal" style guide this?
https://google.github.io/styleguide/cppguide.html
Nit: parent didn't call it an internal style guide, but a style guide for their internal C++.
(I'm sure it is.)
Nit nit: I don't accept your quibble, I think my usage was well within English usage standards; I even put "internal" in quotes! Consider this hypothetical conversation:
"Is the style guide they use for internal projects the same as this style guide that they have published externally?"
"could you clarify which ones you're talking about?"
"Is the internal style guide you described the same as this one I found in google's account on github?"
"oh, I see what you mean"
will you send your second, or shall we simply pistols-at-dawn?
I think this is a tough one, and different people are going to interpret it differently.
The fact that you put "internal" in quotes suggested to me a mild level of sarcasm or disbelief, i.e, I read your message as "You mean this style guide, published on the internet, for all to see? Clearly that's not 'internal'!"
Either way, to me, "internal style guide" (regardless of any quotes placed around any word) means "style guide that is internal" (that is, the style guide itself is private or unpublished).
But the person you were replying to called it a "style guide for internal c++ code": that word ordering makes it clear that "internal" is describing "c++ projects", and that the internal/external (or unpublished/published or private/public) status of the style guide itself is not being talked about at all.
(As an aside, if the commenter upthread had instead said "internal style guide for c++ code", that could have also meant the same thing, but would have been ambiguous, as it wouldn't have been clear if "internal" was describing "style guide" or "c++ code", or both, even. But "style guide for internal c++ code" is about as unambiguous as you can get.)
you are mistaken how English works, how punctuation works, and how context works wrt parsing and semantics. Freighting what I said with your misinterpretations is on you, not me.
what noun cluster would you use for the style guide that google uses internally? Their internal style guide is perfectly accurate. If they publish it externally, does make it no longer their internal style guide? Nope. Would it make somebody exploring the topic wish to add the clarification "oh, their internal and external style guides are one and the same"? Yes.
None of that conflicts with what I wrote, but it does conflict with what you wrote.
“External internal style guide” vs “internal external style guide”, both could make sense in different contexts.
Like I said, I never wrote c++ there so I’m quite likely misremembering details, but IIRC the published style guide linked to upthread is more or less a snapshot of the previously-unpublished style guide used internally for internal code. It may have some omissions and it’s quite likely out of date.
I’m just amused that this discussion about semantics reminds me so much of the gif linked from https://news.ycombinator.com/item?id=40882247
Uhm, no. I think you've got that completely backwards. Agree entirely with what they said, not at all with your interpretation. I think you're flat-out wrong here.
Consider that the style guide that AirBnB uses for internal projects is not the same as the style guide they publish externally, and you can sympathize with why the distinction matters :P.
"the distinction" is literally the question I was asking to clarify, so telling me the distinction matters is divorced from the reality of my comment
Yikes! Tough day today?
Nit^3: this point would have been effectively conveyed as '"Google's internal style guide"'. By putting only "internal" into quotes, you call into question whether its public existence invalidates the internal nature of it.
Whereas the respondent said this:
This is a style guide for definitely-internal c++ code, with the internality of the style guide itself unspecified. I'm not sure what the effect of the scare quotes around "style guide" is meant to be, just that it doesn't move the internal part next to the style guide part.
Putting the whole thing in quotes, rather than just "internal", questions whether the guide you found is the guide referred to, rather than the internal nature of the style guide itself, which the quoted sentence takes no position on.
This has been your daily dose of Hacker News style guides for discussing style guides.
Yes, modulo some text that's inappropriate to post externally. (Often, because they reference internal libraries in google3 -- this includes things like the ban on <thread> and <future>).
Is it really 3/4ths the language? (mostly culled libraries, or features?) I remember reading an old pdf published by the US air force about the subset of c++ features you're allowed to use for contracted software, and it's so different it may as well be a different language.
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.
There are a number of standard libraries that we (Google) ban because we have in-house alternatives that we generally prefer. (In some cases, this is due to compatibility with our threading model. In others, it's probably due to inertia.)
From a skim: <chrono>, <filesystem>, <thread> (along with other concurrency libraries like <mutex> and <future>) are the main ones.
As far as language features that we ban, it's notable that we ban exceptions. Rvalue references are generally discouraged outside of well-trod paths (e.g. move constructors). We ban runtime type information (dynamic_cast, typeid, etc). There are some newer features in C++20 that we haven't accepted yet (modules, ranges, coroutines), due to lack of tooling, concerns about performance, and more.
Sometimes these bans get reversed (there was a long-standing ban on mutable reference parameters that was overturned after many years of discussion).
One of the key points we try to emphasize is that readability is primarily about reading other people's code, and so we provide well-trod paths that try to avoid pitfalls and generally surprising behavior (e.g. this article's focus on initialization? https://abseil.io/tips/88). There is value in moving closer to Python's "There should be one-- and preferably only one --obvious way to do it."
We assert that one of the goals of the style guide (the second section, after Background) is that rules should pull their own weight; we explicitly cite this as a reason why we don't ban goto. I imagine this is also why there isn't an explicit ban on alternative operator representations (e.g. writing `if (foo and bar) <% baz(); %>`).
I don't think I agree that every rule pulls its own weight -- I like to cite that I copy-pasted the internal version of the style guide into a Google doc earlier this year to see how long it was... and it clocked in at over 100 pages. But maybe that's an indicator of how complex C++ is, where we have to make the tradeoff between being concise, being precise, and providing sufficient context.
C with classes and text-template generics would be an ok subset of the language, if external concepts didn't keep creeping into its semantics. The problem is that they do.
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.
“There are only two kinds of languages: the ones people complain about and the ones nobody uses.” - Bjarne Stroustrup
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.
I think we can say Rust is beyond the “nobody uses” stage by now, and it’s much simpler and easier than C++. (And people who use it tend to like it, proving Bjarne wrong).
Rust is neither simple nor easy. Full stop.
Where did I say it was?
Well you did say: it’s much simpler and easier than C++
And if the basis of c++ is c with classes, you’re horribly incorrect. C is a small language, easy to understand and hard to master.
Rust is fucking miserable to understand, and as such, hard to master.
Why are you being pedantic?
C++ isn't "C with classes" and hasn't been for years. Yes, Rust is bigger and more complicated than C, but much less so than C++.
I'm not. I'm making a real point: Rust is much simpler and easier than C++, so the spirit of Bjarne's quote, which is that for a language to become popular it necessarily has to have as many drawbacks as C++, is wrong.
I'm sorry; you think people don't complain about Rust? There are tons of articles posted here from people complaining about Rust in various ways. Bjarne wasn't saying whether most people like it... that's orthogonal: I actually like C++, yet I have been complaining about it--at times quite bitterly--since before it was even standardized!
Indeed, I am a huge proponent of Rust and have been using it since before 1.0 (even contributed to it, in the past) -- and I complain about Rust a lot, too. Trying to restate Bjarne's point here: if I wasn't using Rust, then I wouldn't have any reason to complain about it.
Or, because there’s so many languages around now, they just use something else. I really don’t like working with Rust myself and so I use other languages.
Saying people complain about something is not the same as saying nobody likes it…
I truly loathe that quote. It is a tautology that is used to deflect legitimate criticism.
And it is not true (for any reasonable reading of the quote). There are very popular languages that don't get the deserved hate that C++ does. Sure, Python is slow, packaging/versioning is painful, but it is nothing like C++ complaints.
I mean, a standard (and stupid IMO) interview question is rate your C++ expertise from 1-10, and if you answer more than about 5-6 you get bounced for lying or not recognizing your limitations, while they gleefully point out Stroustrup wouldn't answer 9-10.
I mean. Python:
Bolted on, very terrible OO that should never be touched
Some of the most batshit insane ideas of encapsulation anyone has ever seen
Some of the most batshit insane return rules anyone has ever seen
Encouraged inconsistency from the language in the form of functions like “Len” that are added because sometimes someone feels it reads better?
Encouraged unwinding exceptions as regular flow control (lol. Yikes)
It is nearly universally agreed that Python has significant code management issues as code bases scale
This is all ignoring debates of fundamental typing issues. Personally, I hate what Python does, but some people seem to prefer it.
Let us not pretend Python doesn’t have some language problems on top of its tooling problems.
That's the worst part. But you have to agree it's the best for throwing small sized glue code around.
You forgot "it is so slow you might be faster with pen and paper".
It absolutely is true! You can certainly argue that different languages get different levels of complaints and hate, but every language that anyone uses gets a non-zero amount of complaints, regardless of severity.
Those are complaints. That is evidence that people complain about Python. You just did it yourself.
But maybe your complaints about C++ are an order of magnitude more plentiful than for Python. And maybe quite a few of your C++ complaints are about much worse things. But that's not the point: they are all complaints.
And that's the problem with the Stroustrup quote: he's implicitly saying that all complaints are created equal, and there's no difference between having 10 complaints or 10,000 complaints (where 3 of the first are major, and 5,000 of the second are major).
It's used, as the GP points out, to shut down legitimate complaints. "Oh, you don't like $REALLY_BIG_HORRIBLE_ISSUE with my language? Psh, whatever, people complain about all languages, I dare you to find another language that you won't find something to complain about." Not the point! Is $REALLY_BIG_HORRIBLE_ISSUE a problem or not? If not, actually explain and justify, with specific arguments, why you don't think it's a problem. And if you do agree it's a problem, stop deflecting attention, admit that's it's a problem, and try to find a solution!
I find it annoying to have to solve a puzzle to make progress solving my intended puzzle (i.e. whatever I’m computing)
You're basically saying the language gets in the way of solving your problem :)
I feel that if the language is a challenge to work with, it better give you your money’s worth. In 2024, there are plenty of other languages with better ROI, if you want a challenge.
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.
C++ is a popular multi-paradigm language that is both cutting edge and 40 years old (more if you count C), there is simply no way around that level of complexity.
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.
C++ needs a different name from multi-paradigm. Java is a multi-paradigm language. C++ is an omni-paradigm language. If there’s a paradigm,
- 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
And if you dare to combine two of the paradigms it supports, you get UB.
What is UB?
It's what happens when you make a union of an int and a float and write it as an int and read it as a float.
Most compilers will do something like "treat the bits like they represent a float, even though they mean something else when they're treated as an int."
But the language spec says the compiler is allowed to send an email to Bjarne Stroustrup with your home address so he can come over and personally set your computer on fire.
Undefined behaviour.
Here's an article on the topic: https://cryptoservices.github.io/fde/2018/11/30/undefined-be...
If you’re looking for a language with less complexity than C++, you’re surely not going to find that in rust.
I disagree. To me, the complexity described in this article is more complex than anything you'll find in Rust.
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.
I would sort of agree, except when c++ was invented, it was even more awful in practice (does anyone remember the chaos around STL and template caches?). So, age isn't really a factor.
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.
Zig looks promising too.
Literal lol... this is not an argument in favor of C++.
In case it's not known to everyone, the title is an obvious nod to "I Have No Mouth, and I Must Scream" [1], a 1960s US sci-fi story by Harlan Ellison.
1: https://en.wikipedia.org/wiki/I_Have_No_Mouth,_and_I_Must_Sc...
That plot summary is... dark. Does anyone know how long the story is? Most of the copies I found online are collections of short stories.
It's a short story, a brief one (~32 kB): https://gist.github.com/neckro/0f3a9ec60be34e3164c6677d4ecc1...
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?)
pc adventure game was good too (i.e., messed up)
The question is which IHNMAIMS character the poster identifies with to have deserved his OOP misery, given all protagonists are imprisoned for life (or for eternity, actually, I believe) as a sentence for the bad things they did ;) Note there's also the adventure game created after the book, overseen and with a script also by Ellison.
I assume AM is filled with so much hate because it's just three billion lines of C++.
https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...
Always obligatory https://mikelui.io/img/c++_init_forest.gif
Thanks, I'd never seen that one!
So horrifyingly true.
If you enjoy this, a few years ago I made the "C++ iceberg" meme (with clickable links!). I've been thinking about making an updated V2 with all the "you forgot about X" messages I've been getting.
https://fouronnes.github.io/cppiceberg/
God bless you for making this. I plan to incorporate several of these features at work in the hope of summoning Cthulu and killing the company once and for all.
What's your favorite "you forgot X"? You should definitely make an updated v2 because every link I've opened from the bottom half has been absolutely bonkers.
Three dimensional analog literals drawn using ASCII? What the flying hell was the standards committee thinking.
Well, I do appreciate your work, and the information is certainly helpful.
But it's a bit like a urologist explaining what it will be like when you pass a kidney stone.
And then find out that the C++ standards committee is working on a new kidney-stone shape that's backwards compatible, but adds more jagged spikes.
I should print this and put it on my wall for all those times when I'm frustrated with Rust lifetimes.
So you hate C++. Great, thanks for your informative insights.