C++ Frequently Questioned Answers (2009)
yosefk.com
yosefk.com
We could nitpick at specific sentences/questions, but at the end of the day the spirit/gist of the document remains true, sadly.
I used to quite like C++ in my younger days (highschool, uni), but once you start working with multiple people on a team, or a larger project, the illusion quickly unravels.
The problem in C++ comes when you always have to make a poor value trade.
If you wanted readability, or maintainability you have to sacrifice performance.
Of you want performance, you have to sacrifice everything else.
A great and easy example of this is that the most succinct/readable usage of std::variant has the most morbid performance, and the visitor API is just... Well, if people find that elegant, then beauty really is in the eye of the beholder.
I still write C++ at my job, but I've no illusions about its grandeur.
I have met a few profiles of people in my life/career who defended C++, and they seem to be present on HN too, so please don't take these gross over-generalisations personally; they're imperfect as all generalisations are, and they're just anecdotal anyway:
Some just like feeling like powerful mystic wizards who have mastered the arcane syntax of invoking eldritch features. An additional layer is people who enjoys feeling like heroes when stuff goes wrong in production and they fix it with gdb keyslaps to dump core and transmute memory.
Another profile, that seems way more common, are people stuck with C++. It's kind of a sunk cost fallacy, they already have a career in C++. And there really was nothing better for a long time (ok there was, but not on the job market, and C++ is heavily lobbied at unis). It's also hard to know anything other than C++, because of how taxing the language is; there's little energy left in you to learn something else.
This is only true of Meyers. Alexandrescu and Sutter are accomplished software architects and have been working for large companies.
You could be right... I hope you are right, but I'm afraid you're not...
> I'm not sure about that. I've spent 18 years programming in C++, for much of that time in a senior professional capacity and am excited to learn Rust for whatever my next project will be. Most of my colleagues and peers feel the same. [...] I like C++ but don't love all of the baggage and am delighted that things are moving forward.
I don't know what the "split" is regarding mentality, and I've definitely seen a lot of experienced C++ devs in Rust communities, but then there's also a lot of people in "Rust bad"[1] circles (you see them on Reddit and HN). I think it might be an even split (as with most polarised opinions), but I don't think that either sort is rate.
[1]: I don't blame these people, btw. If you're in a senior or leadership position and you choose C++ for a new project, because C++ is your strong suit. Then you read online "C++ bad" comments that glorify Rust. It's hard to not take these things personally, when it might be affecting your livelihood. Especially if semi-technical management "drinks the Rust Kool-Aid" (for a lack of better expression) and grills you over your decision. It's not a very disheartening situation to be in, and that frustration can fester into contempt, and so on.
That's a good point. In the domain specific communities, I've not not really seen language flamingo.
C++ seems perfect for these people because there are so many little traps and arbitrary rules to remember. “Ha ha, you wrote for (const auto x: y) instead of for (const auto& x: y)? Stupid fool!”
(Or is it auto&& now? decltype(auto)? I honestly don’t remember and don’t really care.)
I’m not remotely suggesting all or even most C++ experts are like this, but anecdotally, their share is higher than among, say, Python programmers.
It’s not like Rust is particularly easy to learn. On a conceptual level, it’s probably harder than C++ (traits, generics, lifetimes). It’s just so much less arcane.
The defaults are wrong. Const should have been the default. Whereupon "East Const" doesn't matter, all the variables which are mutated get labelled. Similarly "explicit" should be the default (nobody wants your random single parameter constructor to be implicitly called, stop that) and "override" shouldn't be (If I name my method foo why is it my responsibility to first check whether some other bozo named a method foo so that I don't "override" it ? The compiler can and should tell me this won't end well†)
† Rust brought me around to the idea that some other bozo's quack() method has no business sharing a namespace with mine just because we're both Ducks, if I wanted to override the shared method I'd have re-implemented Duck::quack() not added my own quack() method. But in the C++ world I'll stick to just saying override shouldn't silently be the default.
yeah no.
struct foo {
int x;
};
would not be compatible with C because the int would be const, that would have made the language particularly useless.I'm not talking about those. I think they plain shouldn't exist, but it's a separate discussion from the discussion about the defaults.
But yes, this means you can't cut-and-paste C code. I appreciate that this is disappointing for the "Actually we already have lots of C++, it's just that it's mostly .c files and doesn't realise it's C++ yet" people. Set your sights higher.
† Yes, this insanity is also available in volatile flavour.
Why would CV members be insane ? Having const members has been very useful for me when refactoring codebases, to make sure that I knew every place modifying such a member. It has never been a hindrance, just plain positive, and not having it would mean one more rule to remember (and having to explain students why you can write "const int foo;" here and not there)
They actually can't (well, shouldn't) write "const int foo;" as a local variable because the compiler needs to know its value. And that gives the game away.
These are already different cases, yes both the structure member and the local variable are names with types, but they already need to be explained separately.
The nice C++ frameworks from the 90's all used bounds checking enabled by default, what does ISO C++? Bounds checking disabled by default and it is up to the compiler vendors to do as they please, hence FORTIFY and _ITERATOR_DEBUG_LEVEL.
If you read how magnificent C++ is on the HILO tutorial or C++/WinRT ones, it is quite clear how WinDev sees these nasty folks doing stuff in .NET at DevDiv.
Joe Duffy on his Midori talk at RustConf mentions that even with Midori being available to WinDev they were asserting it wasn't possible.
A nice (and very ridiculous) fantasy, but no.
As a person who hires developers to write performant code, I'd love it if there were these magical junior Rust developers who can write performant code.
But as the market exists right now, Rust developers are twice as expensive and the product they deliver is twice as bad.
The only way I can justify Rust to higher management is selling it as a vanity PR project.
It's interesting how we have different experiences regarding this. In my experience, it's usually the opposite. And I believe it's mostly because Rust lets you focus more on the problem you're solving, rather than thinking about dos and don'ts of the language.
For example, most Rust projects (or products) seem to have much more polish than equivalent C++ projects (even for ones with years head start). By polish I mean both user experience and the developer experience when one works on the project.
Also, adding user-facing (i.e. UX) polish to Rust projects requires less ahead-of-time planning in my experience.
One reason is that when introducing new Rust code, there's less performance gotchas, and Rust code seems to compose more ergonomically than C++ code... Most of the time at least; there were times that Rust was frustrating. But I think less often probably, _and_ the frustration is compile-time, rather than runtime. Also Rust has a shorter feedback loop due to Rust's incremental compilation really helps mitigate the frustration, so you can iterate faster.
And another is because I've always felt like refactoring Rust code (even with C++ experience than Rust) tends to be easier, and faster to refactor, and I feel bolder while doing it, if that makes sense? Simply because the compiler thinks about various categories of problem domains instead of me, and it tells me about them.
Of course, it's never black and white with any of the above things I mentioned; the situation is grey in both ecosystems. But I think it's a much lighter shade of grey in the Rust space than in the C++ space. And as always, there's always exceptions on either side, too.
EDIT: I quoted OP's full sentence, but then only addressed the last bit. I hopefully clarified in comments below that I wasn't talking about the first part. Sorry about that!
Obviously your experience doesn't involve budgeting or hiring developers.
Your subjective feelings about how nice or fast it is to write Rust code isn't what we're talking about here.
The original poster claimed that Rust can save money by letting managers hire junior devs to replace senior C++ developers.
This isn't true. Rust developers are much more expensive than C++ developers, and have vastly lower productivity. (No doubt because of their vastly inflated expectations; any Go/Python/PHP developer who upgrades to Rust suddenly thinks they are elite 10x programmers because they have a clue about performance now.)
Ah, that is very true! :) I forgot to make clear in my comment that I wasn't referring to that bit at all.
I only participated in the hiring/interview process, but that's as far as my hiring experience goes.
> The original poster claimed that Rust can save money by letting managers hire junior devs to replace senior C++ developers.
Ah, I didn't interpret it that way. I thought the point they wanted to get across was that it's easier to get the same for less—not in terms of monetary cost, but in terms of less mental effort with regards to context/knowledge about the language itself.
But, the hiring point you're addressing can be inferred from their comment, too.
I think I agree with you on that... It probably is "easier" to hire C++ developers, since there's an older and bigger market for them.
I think Rust developers are in an interesting position right now that they're not as abundant as other developers, since Rust is probably still in "early adoption" stage. So they can use that as leverage in salary negotiations, maybe? But it also comes at a cost of a much smaller job market for them, too.
No, you are viewing that from a technological perspective - what you assume only holds if "Rust" was a requirement.
If staffing a project is much harder (and maybe more expensive) when using Rust as language, then Rust will not be chosen if C++ is an option.
This is not at all what I said, nor meant. I never said anything about the hiring market, but merely stated a personal observation why I think Senior C++ devs often dislike Rust.
Senior C++ developers are not impressed with Rust because it doesn't solve any problem they have. Switching to Rust would make them markedly less productive, with no corresponding benefit. The drain comes from slow compilation and from excess design overhead, the need to distort a simpler architecture to satisfy the borrow checker.
Put simply, the extra work to satisfy the borrow checker prevents errors they do not make.
This contradicts Rust propaganda, where a few CVE numbers are cited over and over, echo-chamber style. What these citations never mention is that they are for bugs in mostly very old code. We just don't need to write code that way anymore. It is not just error-prone, it is more work, so there is simply no temptation to write it that way.
I do hear of programmers who have elected to stop learning, and (proudly!) admit continuing to write C++98 or even "C-like C++". We used to say "you can write FORTRAN in any language"; there is no eliminating bad programmers, but plenty of reasons to avoid their product. What conclusions can we make about Rust from its worst adherents?
It is unavoidably true that grave security holes get found only in existing code and not hypothetical future code written with a later language revision and whatever new features will definitely avoid those "errors they do not make". That's time's arrow for you.
Can you name a few of these "Senior C++ developers" and point us to, say, a few years of their work?
Is there a specific vintage of C++ where you feel like, at last, unlike C++ 98 you "don't need to write code that way" and so Senior C++ developers won't make these errors? C++ 11 maybe?
you mean a problem blithely ignored until the software becomes important enough to attack, at which point, it's too late to rewrite the "very old" code? https://googleprojectzero.blogspot.com/2022/01/zooming-in-on...
> errors they do not make.
bullshit. a quick look at oss-fuzz on c++ projects, many of them in modern c++, will show that they are teeming with memory unsafety.
> there is no eliminating bad programmers, but plenty of reasons to avoid their product.
on an unrelated note, what product are you responsible for, so i can avoid it?
> This contradicts Rust propaganda, where a few CVE numbers are cited over and over, echo-chamber style. What these citations never mention is that they are for bugs in mostly very old code.
Empirically this is not true. https://twitter.com/LazyFishBarrel/ is not citing "a few CVE numbers over and over"; for example, in browsers, new code is disproportionately responsible for vulnerabilities. I know most of the people at the big tech companies doing the research into where the security problems in major C++ software come from; they're not making the results up.
What? Why? I've been developing C++ for 10 years and seen firsthand the problems it causes and that are addressed by Rust. If I start writing Rust I'm going to be more productive (from build system to maintenance), write more performant and reliable code faster, so I don't see how the delivered product would be worse, as I'd have more time to iterate.
About the rate, maybe that would be more expensive, but I'd say it's still worth it. Anyway, it's not like getting competent C++ developers is cheap either.
As for GP's message, I agree it looks unrealistic that "kid fresh out of college" would magically write performant code because they're writing rust. There is more to performance than the language, and it is particularly easy to write bad performance code in rust (and in C++ I must add). That being said, I'd still be less anxious to have a junior in a rust codebase than near a C++ one.
Maybe it's true for you, but I can hire a competent C++ programmer to deliver code faster for less than what you'd ask.
Also, if a programmer is spending time "iterating" on a performance problem, he's already underperforming. That's okay for less experienced developers as part of the learning process, but expecting it to be the norm? No.
> About the rate, maybe that would be more expensive, but I'd say it's still worth it.
Real-world metrics do not agree.
I don't think the OP meant iterating on performance problems specifically, just generally iterating on code as one does when they write it.
Also, every programmer at some point iterates about performance problems, not matter their experience, or language (or implementation), or runtime that's being used.
I think the original "uni grad can write better Rust code than experience C++ dev can write C++" is a hyperbole. I don't think they meant better, but the Rust developer will need less context to implement the same feature.
The spirit of that sentence was probably "it's simpler to write performant and safe code in Rust than C++". But again, without really agreeing what "performance" means here, it's a bit silly to discuss.
Performance is such a complex that can mean so many things depending on the problem. So, I we're all nit-picking about something without even agreeing what they're specifically nit-picking about.
The default IO mode in C/C++ is buffered. Rust requires you to wrap it in `BufWriter`. For most network (or general IO, really) workloads you want buffered IO.
So sure: We could say that writing performant IO code in Rust is more difficult, if we hyper-focus on this one specific detail.
FWIW (and people who're unfamiliar with this): I think switching to buffered IO in Rust is absolutely trivial, and very well documented (in multiple places where it's relevant). In fact more so than it is to switch to unbuffered IO in C++, because you have to call `setbuf` with "special" values to disable buffering.
We can then argue that experience/wisdom diminishes most of these arguments about performance, but it diminishes them on both sides.
Exactly, on both accounts, thanks.
It strikes me as odd that GP would describe "iterating on performance" as "underperforming", unless GP is in a very specific domain where the performance requirements are always known in advance and where they never change. Getting the last drop of performance from code is a trade-off. It impacts the readability and maintainability of code. To get the last drop, it can depend on the shape of your expected input data, and it requires looking at the produced assembly and performance profiling results to find bottlenecks and act on them. This alone implies iteration. If you're telling me that your engineers know their compiler so well that they can tell in advance what assembly code will be generated when changing seemingly unrelated parts of the code, eg they know the inlining limits of their compiler, I call BS.
Iterating on performance takes time to develop and maintain, which is why it should be done just enough to meet the requirements, in particular in the hot loop and to reduce bottlenecks. If your developers are optimizing all the code they write to the ASM instruction, they are probably underperforming (unless specific domain where so little code is written or the target is so small that the benefit is worth the cost). Understanding that trade-off is part of seniority (aka juniors will often both "prematurely pessimize" by writing inefficient code without increasing maintenability or in the hot loop and "prematurely optimize" by avoiding allocation at all costs in CLI argument parsing).
I'm optimistic about Rust but realistic (I think) about how much inertia the common project (C++) really has. I'm also decidedly skeptical about Rust's cure-all possibilities - while I've seen a dozen or so real lifetime bugs in a decade, about half of them were caused by a developer misusing an API, deliberately calling a method that is for transferring lifetimes, and then not assigning the reference to a new object.
In my experience, most bugs are just badly written code, not the sort of problem your compiler can fix.
I think this "make a list" idea performs best where you know exactly how many things will be in the "list" but have little idea how long the concatenated String might be, so you can keep the "list" as a local array (no allocation) and do a single String::with_capacity (one allocation) and then push all the references onto it.
Otherwise, obviously we're not going to go around assembling a LinkedList since this is the 21st century and we're not crazy, so let's assume if we don't know how long that "list" is we're accumulating references in a Vec<&str>. We don't know how big to make the Vec either, and so that will grow as we push items onto it.
Rust's Strings are - under the hood - basically a Vec<u8> that promises to be valid UTF-8. So we're playing with the exact same allocation algorithm and same amortized costs in String and Vec<&str>.
If we're appending very few Strings in the typical case, the "list" approach costs extra because we're doing a bunch more allocations + copies to make the Vec<&str> that we're throwing away shortly afterwards. Likewise if the strings are very short.
And after all that it's more complicated, which counts against it unless performance was critical.
If I saw a junior reflexively doing this (with a Vec or some other "list" structure) in code I was reviewing, I'd ask them why. If their answer is "performance" I'd remind them: If we aren't measuring it then it isn't performance it's just wanking. So, where's the measurement?
But I see your point - I inadvertently gave an example that is performance basics in some languages and fine tuning in others.
The lock example is perhaps more relevant.
Another good example is appropriate caching. Often services want to parse something (e.g. an ini file) and cache it in memory. Caching the right (fully parsed) format will lead to a highly performant configuration system - taking less than 1% of CPU time - but let some juniors do parsing on top of the caching layer rather than underneath your caching layer - and suddenly your config system is 5-10% or your service CPU and is a target for optimization. Most of my daily work in growing juniors is patterns like this, nothing a language would help with.
Well, still waiting for that...
"Template specialization and traits? This belongs in library code. The resulting code is actually more bloated than the copy-pasted version. Nope."
How is that possible?
Templates can also expand on each type they use, or combination of types, generating much more code, when non-template solutions could have avoided it. Of course, templates could avoid it too, but they're much harder to write and see how they expand.
That said, templates also allow making code shorter and cleaner.
So some people, once bitten by bad templates or bad usage, complain without learning how to use them. Yet another C++ ocean of complexity :)
Then the blocks weren't exactly identical, so the template (again, not necessarily C++) needs to gain placeholders/ifs and the instantiations need to fill them/provide conditions.
If you do it with C++ templates using the traits machinery, it generates quite a lot of bloat. It is not a reason to never use these mechanisms, just to be wary of the costs.
That's exactly the part that I don't understand. How does using traits machinery generate more bloat than some other way to de-duplicate the same code? And what kind of bloat do you have in mind -- source code or binary?
For the record, OOP is a bad fit for most problems. The overwhelming majority of actual template use has nothing to do with "template metaprogramming", which has been superseded by modern language features he evidently knows nothing about.
Modern features in C++ are not there to pad resumes. Each is carefully selected from a much larger number of proposals as demonstrating it enables writing cleaner, clearer, more reliable, faster code. If you choose not to use any of them, you are perforce choosing to write less clean, less clear, less reliable, slower code.
And the sheer audacity to think only your method produces such results. Grow up.
When I think that in my industry, good mastership of C++ is, what, 30% of what it takes... Throw in real-time design principles and a few different kernels, several CPU architectures, unavoidable Linux and a one or two build systems (Yocto anyone?). And only then comes the real problem domain, communications protocol stacks or image-processing algos, or what have you.
Many modern languages don't have "proper" object-orientation, with subclasses, polymorphism, liskov substitution principle and so on. Rather, they have compile-time polymorphism, or opinionated other ways of doing it (composition in Go, traits in Rust. Methods in Nim are wierdly attached to the language, and if you want interfaces, the author recommends you use a closure and function pointers...)
Other "classic" object-oriented languages are all managed (C#, Java).
What I'd want from a language is:
- a powerful but ergonomic type system (stuff like Nims "distinct" types are nice). Maybe we can even get simple dependent types.
- Class-based object orientation is nice for 1) writing UI code and 2) getting rid of duplicated ifs everywhere.
- Generics. Bonus if we can control contra/co/invariance of type arguments. (A Ptr<ToggleButton> is a Ptr<Button>, but a List<ToggleButton> might not be a good List<Button> everywhere, since you can add ToolbarButtons for example.)
- Functional programming: Functions and methods as first-class-citizens, built-in understanding of pure functions and const data
- Async programming: async/await as first class citizen
- Compiles to native code
- No frickin UB.
True, but there have been always ways to compile them to native code, granted not all of them as free beer.
You put your dynamic components into references and smart pointers of the form Box<dyn Component>, where Component is a so-called “object-safe trait” (which is exactly what you’d call an interface in the OO world). These traits can inherit from each other, and you can upcast a Box<ConcreteType> into a Box<dyn Interface> into a Box<dyn ParentInterface>.
Still, the improvements on those eco-systems, increasingly make it so that I need turn to it less and less.
C++ is one of my favourite languages, it was thanks to it I some major milestones in my career took place, however it is starting to feel like all those critics than one would find talking about PL/I and Algol 68.
The "C++" that is heavily lobbied at unis is still what I would call C+, with some exceptions at what are probably elite universities.
Just yesterday I was reading an ISO C++ paper that touches this issue, P2000R3 - Direction for ISO C++.
We need replacements, because no matter how modern C++ one might want to write in clean C++20, there are millions of lines of gone that go back to the pre-C++98 days, and plenty of mixed quality projects.
However, until Apple, Google, NVidia, AMD, Intel, Sony, Nintendo, Microsoft, ARM decide for something else on their SDKs, C++ is going to keep to be one of the tools to reach for on their platforms alongside the other platform languages, it is what it is.
I can't say that I'd think C++ would be a good choice. Perhaps someone who teaches C++ to undergraduates can offer their opinion about how that works out.
P2000 is an interesting document, and so thanks for causing me to read the current revision - but I'm not sure what you meant by "We need replacements". What is it that you want to replace? The C++ language? The C++ programmers?
I d say it s important to learn a bit just for the C with classes aspect. Sometimes you want to do a moderately complex C program that Java dev will have to help with, and it helps knowing C++ (as in: I did a 3D engine in it, using no overly fancy language feature, for me that s good enough to get an ability to produce something useful enough).
It's a bit sad Rust and Go are so far from C. Might as well just go to Java, a paradise for team work and problem solving.
All the major ones you can think of.
I certainly wouldn't manage a C++ interview quizz, even though I try to keep up with it.
The university where I studied during the 1990's, C was already on the way out.
First year students got Pascal and C++ with proper set of vector, string and other common data structures. This was a couple of years before C++98.
All data structures used bounds checking, and it was great, because the professor has a similar point of view as I, so C++ was taught as a kind of poor man's Ada in regards to safe coding.
What about C? Well, those taking OS classes were expected to be able to dive into it from C++ point of view.
However, this isn't normal, most institutes follow the more traditional path of C with classes, and most of them, hardly moved beyond C++98 anyway.
There is no excuse for the malpractice of university departments. One could blame tenure, but the grad students assigned to teach don't have it.
Apparently in your eagerness to once again argue for C++ in yet another C++ comment you translated "naturally Scott Meyers can't write books forever." as if I hasn't aware of the fact that he has left the community long time ago.
Apparently those words cannot have any other meaning as I am out of touch with what Scott Meyer does, otherwise how could you use it.
Anyone that bothers can assert many of my statements with public stuff from me on the Internet, they can even find naive Usenet stuff from me all the way back to 1994.
Prove us that your "expert C++ knowledge about the real market usage" is to be taken seriously and not polluting HN with C++ zealotry.
He surely can't.
Is pretty much true for any generally used language X. You can write security bugs, invalid domain logic, code that kills people etc. in any language that’s popular today. However there are examples of proven correct software (CompCert, CertOS, seL4 etc.) so we will hopefully get there eventually.
This is true in every programming language to some extent. What makes it worse in C++ is that the language markets itself as being performant without any trade offs (zero cost abstractions!) and the reality is very different from that.
You probably mean zero overhead abstractions. The idea is that you can't outperform it by handwritten equivalent code.
I would say that, at least as of 2011, this is not true. However - I would posit a 3-parameter tradeoff:
1. Better Readability-and-maintainability 2. Better Performance 3. Less coding effort
If you want (1.)+(2.) you won't get (3.), i.e. you'll have to work hard.
You may be able to write performant code relatively quickly, but it will tend to be more difficult to read and maintain; and you can write readable, maintainable code relatively quickly, but it may not squeeze out what it can in terms of performance.
But for C++ there is little effort to bridge those gaps, or at least it feels that way. Maybe that's unfair of me, because C++ is very keen on backwards compatibility, though; I disagree with how religious they are about that goal, too.
Rust on the other hand strives to make idiomatic and easy-to-maintain/write/read code perform well. There are obviously high-level decision that play a role in performance, but I'm talking more about "vocabulary features" of the language/ecosystem, not the highlevel decision of "Do I use a List, a Map, or a Vector for this?"
Sure, there's ugly/unsafe and hard-to-understand parts in Rust libraries (and stdlib) too, but those warts don't leak into the "consumer" code (at least not as much). Also there's consistent effort to add DX polish to tooling, that's not such a "cultural thing" in the C++ ecosystem
As an example, for years C++ didn't have std::string::contains() because you could write find(x) != npos -- and so the argument is why carry around this unnecessary function? Well, because that's what programmers often actually wanted. C++ 23 will finally get std::string::contains()
Yes, because you would have a utility header where you implemented it like so:
bool contains(const std::string& s, char c) noexcept
{ return s.find(c) == std::string::npos; }
> and so the argument is why carry around this unnecessary function? Well, because that's what programmers often actually wanted.No, we didn't want the method, we wanted the function. Actually, string has way too many methods, most being useless, in the sense that they are trivial to implement as freestanding functions.
But really, what programmers (should) actually want is uniform function call syntax: https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax - which would let us blur the distinction between freestanding functions and methods.
I know C++ better than it deserves
and Finally, the following sentence is only here to attract a certain set of search
engine users interested in our subject, and should not be interpreted as the
summary of my point of view on the complicated issues discussed:
C++ SUCKSC++ Frequently Questioned Answers - https://news.ycombinator.com/item?id=887722 - Oct 2009 (28 comments)
C++ Frequently Questioned Answers - https://news.ycombinator.com/item?id=602981 - May 2009 (73 comments)
The first parsing problem is what I would call textbook parsing, as this is what is usually presented in books on the topic. It is a decision problem: the answer is either "yes" or "no". For context-free grammars, including C++, it is always decidable. So what about the ambiguous `AA BB(CC);` given in example here? Is it an object definition or a function declaration? The (decidable) textbook answer is simply "yes". The key thing to understand here is that textbook parsing does not require resolving any ambiguities. The "context-free" in "context-free grammar" only refers to this decision problem, which is why C++ has a context-free grammar, because textbook parsing does not require looking at any surrounding context.
Of course, if you are writing a compiler, "yes" is not a very useful answer. You want either "yes, it is an object definition" or "yes, it is a function declaration", which turns the parsing problem into a completely different one, which I will call compiler parsing. Compiler parsing is not a decision problem, and in the case of C++, it is undecidable.
What is meant by "all popular languages have context-free grammars" is simply that most languages have an unambiguous context-free grammar. To add to the confusion, textbook parsing and compiler parsing are essentially the same problem for unambiguous grammars.
I would love to see a backward-compatibility compiler and library that is essentially whatever C++ is now, frozen in time: any “.cpp”/“.cc” file is simply built with that. All new C++25 code should be something else (maybe “.ocp” for Opinionated C++?).
The “.ocp” files would build against a complete replacement for the compiler and standard library, offering ONLY the sane subset of the original language and library that we have always needed. Finally remove all these constructs that we are not supposed to be using anymore. Throw std::iostream into a dumpster and set it on fire. Refactor all of this code that had to be implemented in weird ways simply because of the desire for perfect backward-compatibility. And finally take the opportunity to have sane defaults, which alone will probably allow 12 different keywords to be stripped out of the evolved language.
I don't think this is specifically a C++ problem. It's true of any language, especially in a team setting.
Reminds me of the post from yesterday, where some people where adding decorators in python to 'unmutate the mutable default argument' instead of just setting it to 'None'.
It’s interesting to notice how “this” is what distinguishes the two.
When creating member functions in C, function pointers are assigned members. In order to access or change the object with an assigned function pointer, that object needs to be called or in other words, “this”.
obj.member_function(&obj);
In C++ would be
obj.member_function();
Or am I (hopefully) misunderstanding something here?
It's pretty pointless unless you want some sort of namespace-like organization. Maybe for polymorphism because you can change an instance's "base" member function pointer.
To make this concrete, Let's go over the "C++ defects" summary appearing at the link, before the individual "frequently-questioned answers".
> changing the private members of a class requires recompilation of the code using the class. When the class is used to instantiate member objects of other classes, the rule is of course applied recursively.
This is mandated by two of C++' language design goals:
1. Performance of compiled C++ code: This recompilation is necessary for exploiting optimization opportunities involved the altered private members. In fact, these days, compilers goes beyond even that situation: _linking_ code may trigger partial re-compilation without any changes to the source.
2. Zero-overhead abstractions: Having a private member should not in itself cost you in terms of performance.
However, we can make the class implementation opaque (e.g. using the PIMPL idiom: https://stackoverflow.com/tags/pimpl-idiom/info ) if we want/need to.
> Outstandingly complicated grammar
C++ is guilty as charged here! It even has a semi-idiom named the "most vexing parse": https://en.wikipedia.org/wiki/Most_vexing_parse
But there is an excuse, which is: Backwards compatibility. First, with C and C's syntax; then, with earlier versions of the language. Fixing the complicated syntax would break all existing C++ versions. So, they don't do it.
> No way to locate definitions
The fix for this is now part of the language: Modules. Unfortunately it will take time before these are widely-enough adopted.
> No run time encapsulation... C and C++ are not designed to run in managed environments
This is again necessitated by the performance requirements. Run-time encapsulation would mean slower code, sometimes much slower.
Also, such encapsulation would mean there would be room for another C++-like language, below C++, without this encapsulation... and a goal of C++ is not to leave such room.
Luckily, in recent years we have undefined behavior sanitizers in compilers, which catch a lot of these cases.
One should also remember that languages with managed runtime environments often have exceptions to the rule, i.e. unsafe/unmanaged sections (unsafe in Rust, JNI in Java).
> No binary implementation rules
Sorry to have to harp on this, but platform-specific ABI is another necessity of performance. Still, C++ has a long way to improve in this respect, e.g. https://stackoverflow.com/q/58339165/1593077
Recently, stack traces have been introduced into the language, and in C++23 they might be available by default when throwing exceptions; see: https://wg21.link/p2370r2
(I may continue in another post or extend this one.)
Even in C I can say:
struct A;
void public_member_function(A *a);
I can also add as many static functions as I like later without affecting the public declararation of struct A.
Why not in C++?
Extra support in the language for automating PImpl types could be welcome, but might not offer enough benefit to offset the complexity.
function(a);
vs.
a->function();
I can accept that some think the second version looks nicer. But I don't buy that we should have to jump through hoops like pimpl just to get there for non virtual calls.
What matters is that your "a" is a naked pointer. Pointers have reference semantics, not value semantics, so are inherently error-prone. A pointer might be null. A pointer might be already freed. A pointer might be left over from some other use.
A PImpl object is a value, with value semantics. It owns the wrapped pointer, and whatever that points at. When it goes out of scope, it cleans up. If you throw, it cleans up. When it is copied, the right thing happens, if that is allowed, or cannot happen, if not. When it is passed to a function, the right thing happens. When it is returned from a function, the right thing happens. The "right thing", in each case, is visible in exactly one place in the source code, and nowhere else. Nothing using it can do any of it wrong.
This is all really fundamental stuff.
----
> No reflection ... impossible to ... iterate over the methods or the attributes or the base classes of a class ... [or] programmatically determine the type of an object...
There's run-time reflection, and compile-time reflection. C++ has almost no runtime reflection - and this is again due to performance considerations. That would have a lot of overhead.
As for compile-time reflection, C++ has gained some over the past decade. decltype() gives you the type of an object. A static reflection Technical Specification is out, see: https://en.cppreference.com/w/cpp/experimental/reflect . It will allow much of what the author was decrying. It may make it into C++23, and some compilers already partially-support it. Now Is it very late in arriving? I suppose it's fair to say that. But TBH, it's quite difficult to design this for a 40-year-old language with a lot of baggage and not within a managed runtime. And achieve near-consensus about it.
----
> Very complicated type system
The type _system_ of C++ is not very complicated. What the author seems to dislike is how common standard-library types are actually somewhat complex constructs within the type system rather than atomic types or simple constructs. A language in which "string" is an atomic type might have std::map<string, string> rather than the complex-sounding `map<std::basic_string<char, std::char_traits<char>, std::allocator<char>>`, `std::basic_string<char, std::char_traits<char>, std::allocator<char>> >`. Still, that longer type is simply `std::map<std::string, std::string>`, and if you `use std::string;` then it's `std::map<string, string>`. (Yes, there are other template parameters to map, I know, I'm making a point).
In fact, one could argue that it is a virtue of C++ of having a non-complex type system: The complex types are actually implemented _in C++_, with no need for a rich gallery of atomic types. Other systems put things like lists, associative arrays, optionals (a.k.a. maybe's) etc. out of the reach of "mere mortals".
Then again, the choice of primitive types or type constructs in C++ is not great due to inheriting from C. C++ can construct new types as arrays-of, structs-of, unions-of, references-to, pointers-to and bit-fields-of. arrays are annoying with their decay behavior and unassignability, and non-discriminated unions are also useful mostly for low-level works. It would perhaps have been nicer if arrays behaved like std::arrays and unions like std::variants, and for the C-style types we would have used reinterpret-casts and other dirty tricks. But then, what would you want to happen when a constituent element of a variant throws an exception during construction? Is the variant in a valid state after that exception is caught? The answer is not at all trivial, so richer atomic types require making choices that aren't always right for everyone.
> if your function accepts const std::vector<const char*>& ... and I have a std::vector<char*> object ...then I can't pass it to your function
Well, not exactly. I mean, you actually can pass it to the function, if you insist: `my_func(reinterpret_cast<std::vector<const char*>&>(my_vec)` ... but that's not very nice. So, yeah, this is definitely an issue.
However:
1. There is no inherent reason that type T<E*> should be usable as a T<const E*>. Those could be completely different types For T = std::vector the types just happen to correspond nicely. I doubt that other languages address this, though.
2. If you want to be a wise guy, and tell me "other language would just have a string and a const string instead",, then - you wouldn't have this problem, since there is no such thing as an std::vector<const E>; that doesn't compile.
3. If your function takes `std::span<const some_string_class>`, you again don't have this problem.
so, a bunch of mitigating arguments + you can still do it if you really want to.
----
> Very complicated type-based binding rules
They're only complicated if you make them complicated. If you single f(), there won't be any complex overload resolution; and if you write a bunch of f()'s, there will.
As for the standard library, it's overload resolution can get complicated; but error messages have improved significantly! First, we got static_assert()'s in C++11 which explain what's the problem with (hopefully) simple text. Then in C++17 we got some deduction guides, so you fail less often. Then in C++20 we got concepts - which themselves are quite complex, but you don't have to worry about them; the point is that your error messages for overload resolution improve again. Don't get me wrong: There will still be long errors about failure to resolve, but they're more readable these days.
----
> Defective operator overloading
The only argument I saw here was:
> overloaded operators have to return their results by value - naively returning references to objects allocated with new would cause temporary objects to "leak" ...
1. In C++, we pass things by value unless there's a good reason not to.
2. If you're worried about copying when returning - that doesn't happen. This used to be a compiler optimization, but since C++17 the language guarantees it: https://en.cppreference.com/w/cpp/language/copy_elision . The return type doesn't even have to be copyable or movable!
3. Don't use new, it's rude these days: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines... ; if for some reason you must allocate dynamic memory, use std::unique_ptr, or std::shared_ptr, or return an allocating container class instance, by value.
---
> Defective exceptions
C++ exceptions could of course be improved in various ways (e.g. they may soon be getting stack traces); but not along the author's line of argumentation.
> If we use exceptions, we have to write exception-safe code - code which frees all resources when the control is transferred from the point of failure
You need to write "exception-safe" code anyway! Acquire resources on construction, release them on destruction. Also known as CADR or RAII.
> ... and the vast majority of "resources" happens to be memory, which is managed manually in C++
Effectively - no, it isn't. And that's exactly because we're writing that "exception-safe" code: Since we _don't_ just write new nor delete (see above)
> To solve this, you are supposed to use RAII ... meaning that all pointers have to be "smart"
1. No they don't - only the _owning_ pointers need to be smart (or in container classes etc.); and the non-owning pointers you don't have to free.
2. What's your obsession with pointers, anyway? Pointers in C++ are so last-millenium...
----
> Duplicate facilities
Yeah, C++ has some of those. I mentioned std::array vs C arrays and std::variant vs C unions. That's the backwards compatibility for you... but also, don't mistake _similar_ facilities for _duplicate_ ones, like the author does:
> If you need an array in C++, you can use a C-like T arr[] or a C++ std::vector<T>
Those are different! `T arr[N]` has fixed size determined at compile time, and its elements typically resides on the stack; `std::vector<T>` has variable size and its elements typically reside on the heap (allocated with new). In fact, C++'s standard library is _missing_ many important array-like classes! See: https://stackoverflow.com/a/67409339/1593077
> That's because C++ doesn't have garbage collection, since that, folks, is inefficient.
It's not just inefficient; by now, it's effectively unnecessary. See: https://stackoverflow.com/a/48046118/1593077