Hacker News new | ask | show | jobs
by gumby 2309 days ago
This decade old article describes an ancient and obsolete language (C++03 probably though some of the text suggests it might be C++98). It's not worth reading in 2020.

Modern C++ is a very different language though it can still almost completely interoperate with quite old code. C++11 and C++14 already addressed most of the things brought up, and contemporary C++ (obviously most code is not in it yet) even supports generic template functions with a straightforward syntax (i.e. use auto instead of <..>).

7 comments

To the contrary, modern C++ has solved very few, if any, of the problems described in the article.

Being generous:

* There's now `std::string_view` to address some of the problems with `std::string`, but the rest are still there. There are some attempts to specify the encoding now, at least.

* Lambdas and `std::function` pretty much solve the function pointer complaints, with some added complexity.

* Containers still do silly things when you use `c[..]` syntax with no element there. (Both when trying to insert and when trying to retrieve!)

* The general level of language size and complexity, especially around templates, has only gotten worse. Concepts will finally help in some ways here.

> To the contrary, modern C++ has solved very few, if any, of the problems described in the article.

The author, after a long and dubious appeal to authority, claims that:

* C++ exceptions are outright evil and should be avoided

* C++ string class "is so utterly godawfully stupid" because it has no garbage collection or refcounting, and in short doesn't precisely match Python's implementation. He also felt personally insulted by the fact std::string is a template class.

* He feels C function pointers are fine but believes function pointers in C++ weren't extended to support pointers to member functions, which he then backtracks and says they actually exist but they are "so horrendously ill-conceived that absolutely nobody uses it for anything", thus showing his ignorance and lack of experience.

* For some reason he criticised std::map for the way it's std::map::operator[] returns a reference to the mapped value, eventhough that's what it is supposed to do by design and by the principle of least surprise.

* The author made other claims, but the text grows so much in the style of "foaming from the mouth" that it's just better to stop reading after a point.

In short, this article is just a rant where someone vents that a programming language other than Python is not Python at the eyes of the author. It's a pretty unconvincing rant and full of logical and rational holes, but a rant nonetheless. So the author loves Python and his personal take on C++ does not match his view of Python. That's fine. It's just weird how this sort of text floats up in a technical forum after over a decade after it has been published, as if it's expected to add or say anything of value.

> "Using the "+" operator with two string constants gives a weird compiler error about adding pointers?"

He'd have that in C too, and that's the expected behaviour. What he would want is:

  #define STRING1 "hello"
  #define STRING2 "world"
  const char* gString = STRING1 " " STRING2 "!\n";
  #include <stdio.h>
  int main() {printf(gString); return 0x0;}
If you make them string constants rather than character arrays with the new literal ""s added in C++14, problem is gone. Author is using outdated C++.

This is all due to C compatibility.

Ah, yeah... no clue about C++'s strings, I just read his question and that sounded familiar to the problem, whose solution I posted above.

There's some overlap between C and C++, but maybe this wasn't such a case? Sorry, if it was uncalled for.

You're not wrong, I wrote off the insubstantial stuff as rhetorical and started my comment with "being generous." :P

But on the other hand, even some of these less-helpful points have an element of truth:

* A lot of people/domains/codebases do wind up avoiding exceptions, for good reason. There is even an active proposal to provide an alternative kind of exceptions to solve their problems!

* Member function pointers are quite a mess, introducing a lot of complexity by not being compatible with regular function pointers. It wasn't until long after this article that those incompatibilities were papered over with the standard library.

* std::map::operator[]'s behavior arguably does not follow the principle of least surprise- it's a giant footgun.

The author goes out of his way to point out that the changes he wants don't require GC or an interpreter, despite all the comparisons to Python. It's not an insubstantial comparison in that light.

Your version:

> The author [...] claims that C++ exceptions are outright evil and should be avoided

What the article actually says:

> This includes the RTTI and exceptions stuff; C++'s versions of those were enough to convince a whole generation of programmers that introspection and exceptions were outright evil and should be avoided

Your reading comprehension is lacking. You didn't understand what he is talking about. Why is Google not using exceptions?

> Containers still do silly things when you use `c[..]` syntax with no element there. (Both when trying to insert and when trying to retrieve!)

FWIW, I find std::map operator[] creating an object extremely convenient and it is very annoying having to use defaultdict to get the same behavior [edit: in python]. So ugliness really depends on what you are used to.

> The general level of language size and complexity, especially around templates, has only gotten worse. Concepts will finally help in some ways here.

variadic templates and type deduction greatly help. A lot of complex template metaprogramming is suddenly not needed anymore (I've used boost::mpl a lot in the past, but not once in the last 10 years). constexpr functions also helped get rid a lot more use cases. Finally if constexpr made a lot of sfinae hacks obsolete.

> FWIW, I find std::map operator[] creating an object extremely convenient and it is very annoying having to use defaultdict to get the same behavior [edit: in python]. So ugliness really depends on what you are used to.

Adding behavior to replace a NULL/None return with a default object is pretty easy.

Removing the default object that you've been inflicted upon... how would you go about that?

if you do not like operator[] behaviour just use map::at
If everyone can forgive my ignorance... I used to call everything in the std:: namespace as just that, the standard template library (STL) (whatever it's iteration). So is this C++03 /C++11 stuff just updates to this library, or is std::string recognised at the compiler level? (Genuinely confused).

(In old man voice) We ended up rolling out own stuff and marking std:: verboten. Why? Stl was too slow, too verbose, and too hard to grok the stack when debugging. We ended up with less memory fragmentation, less dangling ptrs, etc etc. In the rare case there was something in STL that was actually cool (or faster, which was very rare), we'd gut it and reef out the part that was cool to use in our implementation.

I presume these comments aren't popular (sorry about that, but this is during 90s and early 00s when dev cycles were clearly different). Eg. We had string classes in all different flavours, some would interop, some wouldn't. Eg. We had tree and hash classes that, while templateable, had a few core implementations that made compilation fast. We had various ptr management systems (ref counted, stack based, etc).

We made STL between verboten in all APIs because we been burnt so many times using (/trying to use) other ppls APIs that exposed STL in its library. (We were exclusively a windows shop, if that helps understand my confusion... PS. I'm retired these days and have been out of the c++ game >10yrs)

> So is this C++03 /C++11 stuff just updates to this library, or is std::string recognised at the compiler level?

std::string is in the STL. So is std::string_view. There are language changes as well that enable some of the new STL additions but some are just STL updates and you could "backport" them so to speak. Or do the same thing but yourself.

Making std:: verboten these days would likely be a mistake, though. There's so many things that are just not controversial and not worth re-implementing. Like std::unique_ptr. Or std::array.

Most of the containers are still better off ignored, though, many of them unfixable. The least worst of them is std::vector and it's still _ok_ but even there things like absl::InlinedVector are worth a strong consideration instead ( https://github.com/abseil/abseil-cpp/blob/master/absl/contai... ). Or boost's small_vector ( https://www.boost.org/doc/libs/1_60_0/doc/html/boost/contain... )

I still think the STL containers are the sane default to reach for & switch to more obscure ones when the problem domain and performance requirements say to use a different one.
You have that backwards. The STL containers are for when you have a hyper-specific niche use case. They are otherwise terrible defaults and everyone should use boost or abseil by default otherwise.

std::map, for example, is only appropriate if you need a red-black tree specifically. Which almost nobody does. std::unordered_map is less awful, but abseil has a literal straight upgrade. With the same API. So... why would you pick the slower thing when you're using C++? std::vector is only really appropriate if you know you never have small vectors, which is again a more obscure situation.

> You have that backwards. The STL containers are for when you have a hyper-specific niche use case.

That assertion makes no sense at all. The stl contrainers work very well as basic generic containers that can safely be used for pretty much any conceivable use where performance isn't super critical. I'm talking about cases like, say, you need to map keys to values but you don't really care about performance or which specific data structure you're using. That's stl's domain: robust, bullet-proof implementations of basic data structures that are good enough for most (or practically all) cases with the exception of a few very niche applications.

If you happen to be one of the rare cases where you feel you need to know if a container is built around a red-black tree or any other fancy arcane data structure, and if this so critical to you that you feel the need to benchmark the performance to assess whether you either need to use non-defaults or completely replace parts or the whole container with a third-party alternative... Then and only then the stl is not for you.

Small vectors break iterator guarantees, for one thing. They also really only make sense for tiny objects (ints, etc.) given you don't want a pickup truck's worth of data on your stack. They're most definitely not general-purpose.

There are lots of subtleties STL containers have to worry about in designing containers, regarding everything from iterator & pointer invalidation to allocation and allocator propagation. All this is because they're designed to be general-purpose and support most conceivable use cases. Their replacements have to trade off requirements in order to get better performance or otherwise improve on some axes.

Because that way you don't need to haul in dependencies unless you have a real reason.

std::function is fine for prototyping, but its size hit is extreme, so in embedded code we use other implementations. But where size and speed doesn't matter? Why bother?

abseil's might have a similar API but it's most definitely not the same (function signatures looking the same doesn't make it the same API). Some of the standard containers can't be fast because too strict/specific standard requirements, not because they don't try hard enough.

Having said that I think using abseil's containers is reasonable, even as a default, if you can afford the dependency.

> std::unordered_map

AFAIK unordered_map is the most awful of all standard containers.

Because C++ is a nice, mostly safe, general purpose language, being spoiled by the 1% "performance above anything else crowd".

I want my OWL, VCL back, not an hash table able to do lookups in micro-seconds

on the contrary, std::map is a good default container with predictable performance. If you need fast O(1) look up std::unordered_map is really not fit for purpose and requires you to come up with an hash function.
Every new standard incorporates language & library changes. A perfect example of this is r-value references. That was a new language feature in C++11 & was adopted by all the standard library containers & algorithms.

Not sure which part of std::string you're referring to but the compiler generally doesn't contain any knowledge of the library itself. It does goes the other way though where the standard library has to know how to implement certain functionality on a given compiler (some type traits functionality IIRC isn't possible to implement without compiler builtins that expose the AST to you). I think Rust has taken a more sustainable approach with their macro system which can modify the AST instead of relying on builtins but even in Rust I suspect they use builtins in certain places of their standard library.

Today's STL implementations are going to be better performing and more robust than anything you'd write yourself so generally a good idea to stick to it as a rule of thumb for the majority of code.

> even in Rust I suspect they use builtins in certain places of their standard library.

There are places in the rust standard library which just omit the implementation because it's magically filled in by the compiler based on the type and function names. Which, for really low-level and fundamental functionality, seems fair game to me.

As non exhaustive list, compilers have have intimate knowledge of:

* various operator new * all the type traits * a lot of the names inherited from the C library

compilers could do much more, but in general they prefer to implement generic optimizations instead of targeting a specific library name (for example removing allocation for stack allocated std::strings was not done until the generic removal of alloc/free was implemented).

Many of the type-traits can be implemented without specific compiler support. That's how Boost managed it. Here's their implementation of is_signed if you're curious [0].

I believe some standard-library type-traits cannot be implemented without specific compiler support.

[0] https://github.com/boostorg/type_traits/blob/059ed883/includ...

That is indeed a very different time. I believe in the 90s the STL was considered too radical in its use of templates to be practical. People's perception has come a long way. Heck, instead of sane std::list<T> people used to do intrusive linked lists by having T inherit from a linked list base class. What a terrible idea.
Intrusive lists are so good they are likely to land one of these days in standard. Except via templates and traits not silly unnecessary inheritance. (Boost-like.)
Yes, boost::intrusive is particularly great.
Do people use std::list? I can't think of a time when I've needed a non-intrusive linked list (either vector is better or I need an intrusive list).
> We ended up rolling out own stuff and marking std:: verboten.

I think that’s still pretty common, especially in game development and/or when compiling with exceptions disabled. AFAIK std::vector is fine, and probably std::string, but for things like std::[unordered_]map, Abseil and Folly have equivalents that are slightly non-standard and much faster.

2 people equally confident in their own, completely opposing opinions. Every comment enabled website in a nutshell.
The real problem is everyone else who pattern-matches confidence with competence and upvotes.
I think this transcends comment enabled websites.
> 2 people equally confident in their own, completely opposing opinions.

Also known as a debate.

And as long as both are honest and well argued, that's incredibly rich.
I think most people agree that operator[] is a big footgun for containers, but rigorous use of const helps at least prevent surprises.
And operator new is hardly used any more in modern C++ code.
> Containers still do silly things when you use `c[..]` syntax with no element there. (Both when trying to insert and when trying to retrieve!)

This is surprising for Java devs, but given that value semantics must be supported, there's no way to avoid it.

Yep. There isn't even a split/join method in std::string ... you need to use boost for that (wtf)
Or Abseil, which had a lot of handy string manipulation functions including Append/FormatAppend.
The author also seems to really, really want C++ to be Python, despite that they're completely different languages for completely different purposes that make completely different sets of design tradeoffs.

In that respect, the author probably wouldn't be happy with modern C++, either. For example, C++ is never going to be a garbage-collected language (by default, at least). Modern C++ gives you better tools to deal with that, but the core design concerns that make C++ the way it is haven't gone away.

Actually C++11 introduced a GC API.
It seems to me that just a couple of do nothing placeholders were added to please Hans Boehm and get him to work on the c++ memory model :).
Even so, there are the .NET, Unreal and COM/UWP programming models as well, which while not taking advantage of C++11 GC, do bring a GC into C++ world. :)
I’m not aware any exist, but implementations of C++11 can have garbage collection by default. https://isocpp.org/wiki/faq/cpp11-library#gc-abi:

”Garbage collection (automatic recycling of unreferenced regions of memory) is optional in C++; that is, a garbage collector is not a compulsory part of an implementation. However, C++11 provides a definition of what a GC can do if one is used and an ABI (Application Binary Interface) to help control its actions.”

That's not "by default". The compiler doesn't provide it, the language provides for you as the developer using the language to implement your own (or use a third-party) GC and utilize it. D and Rust both offer the same amenities and are not "garbage collected" languages.
> D and Rust both offer the same amenities and are not "garbage collected" languages.

Rust isn't but D is absolutely a garbage collected language. The GC is provided by default and expected to exist. https://dlang.org/overview.html#resource & https://dlang.org/spec/garbage.html

There's a non-GC'd subset of D, though. That would be the BetterC subset https://dlang.org/spec/betterc.html

I think it's more accurate to say D's GC is expected to exist if you want to use the full language. That's sensible, because the option to use GC makes some things practical that otherwise wouldn't be. You can disable or simply avoid the GC, and you can add @nogc attributes to your code if you want to be certain there won't be any GC allocations. BetterC certainly does guarantee there's no GC, but that's a limited (for now at least) subset of the full language.
The quote seems to be about the compiler/environment being permitted to offer GC, ie. permits you to write C++ implementation on top of some GC'd runtime. It does not say anything about you as a language user having enough introspection capabilities to write an GC. As a side note it seems that one can (ab)use smart pointers enough to build essentially working tracing GC on top of that, I've seen that done and well, the performance is horrible, obviously.
You wish. Unless I'm terribly misinformed, "modern C++" contains all of the features of the prior versions, which means that in the real world you have to learn and deal with all of it.
That is the point of backwards compatibility. It is a feature, not a bug.

Whether that feature is a good idea or not and to what degree, is the question.

I'm not saying it's a bug. I'm saying that backwards compatibility means that you still have to deal with all of the historic flaws of the language today.

The article is indeed still relevant.

But that’s true for literally everything. If you have a cpp code base and are building new stuff in rust to interop then you still have legacy cpp code even though rust has fewer problems.
Not really. Legacy stuff can be contained. For example, you could specify language version level in file and it would disable removed/deprecated stuff.
Linters can prevent you from using outdated C++ constructs with ease.
... but it's worth considering that this is a 'question' from 2010. Just saying...
I don’t really understand your point. Just because all the implementations support the `asm` keyword doesn’t mean you need to know the assembly language of every CPU that has a c++ compiler.

You can write your code in modern c++ and ignore prehistoric carbuncles (some of which have been deprecated and eliminated FWIW). And you can call external code written in older dialects without having to look into its source code.

That's great for a greenfield project where you're the only developer. If it's multiple people, you have to deal with whatever subset the powers-that-be on your team wish to use. If it's not greenfield, you have to deal with the subset exists in the codebase.

And if you're a Dirty Harry type like me, debugging issues with dozens of disparate codebases written in various crap subsets, you pretty much have to know the whole cursed thing.

Not everything; see for example std::auto_ptr.
Interesting (and also another thing to know).

Is there a list somewhere of features that have actually been removed from C++? (i.e., that would cause old code to break)

Here’s a list for C++17 and C++20, at least: https://mariusbancila.ro/blog/2018/07/05/c17-removed-and-dep...
> which means that in the real world you have to learn and deal with all of it.

This is not what happens in the real world though.

While I agree that you have to deal with whatever is in your codebase at a given point, it also doesn't imply that you have to use everything and that everything can be useful for your project.

As standards keep coming and features get added, it's still increasingly prevalent to see guidelines and awareness around the topic of choosing your own subset of C++ to work with.

The main drawback is mostly that it incurs a cost in terms of brain power to discipline yourself to keep your work within a restricted set of language and standard library features.

It's extremely rare (and even debatable) whether a single person masters all the aspects and features of C++ (and if there's one, it's probably Alexandrescu).

In my world (see other reply), I get to deal with whatever is in dozens of codebases written in many times and places. The superset of everything they're using is pretty much everything.

Beyond that, even the "good" modern subset of C++ is crazy complex. I've sat in a room with some of the better C++ programmers in the world while they study and try to comprehend the new features like rabbis interpreting the Talmud.

If there's one concise, fundamental rule of software engineering, it's this: Complexity kills.

> which means that in the real world you have to learn and deal with all of it.

That makes as much sence as claiming that to develop software in, say, Python on the real world you have to learn and deal with all of its standard library.

That is not the case. That has never been the case ever for any programming language, be it large or small. Specially in C++, where since it's inception the standard approach is to, at best, adopt a subset of all features and stick with it.

Making a language so big and complex that you can't expect to understand it all isn't a good idea. You might be able to get away with using 50% of the language but if some project you depend on uses a different 50% then you have a problem.
IMHO C++ is unfairly singled out with regards to it's extension. That criticism rings true if applied to pretty much every single programming language ever devised, including "small" languages such as C.
C, even today, fits entirely in my head. C++ does not.

I suspect this is not an uncommon situation among programmers.

OK but if you're a competent, adult software engineer it should be fairly easy to overcome that problem.
Then why is it so common for people to try and select a subset of the language. Arguing about what people should do is pointless, you have to look at what they do do.
If there are multiple ways of achieving the same result (e.g. assignment) and you want to be consistent, you have to choose a subset. If there are features that time has shown to be less-than-ideal (e.g. malloc/free) you choose a subset while the language avoids removing breaking backwards-compatibility.

So when you go to another project that made different decisions, what you should do is understand the decisions. It's quite easy. And that's why it's what people do do, and it's why people are capable of contributing to projects other than their first.

If you program in C++, it's the same skill you used to learn one or more of {CMake, Makefiles, scons, bash, python} except it's easier because you already understand the programming language's model.

> OK but if you're a competent, adult software engineer

"No true Scotsman" and all, please refrain from posting messages that add nothing of value besides petty insults thrown in broad brush strokes.

Much as I didn't mean to insult anyone with my post, I'm sure you meant to add value by calling it petty and fallacious.

Picking up features in a language you already use is easier than learning the language to begin with which itself is easier than learning to program at all. If you were able to read the documentation to get to 50%, you have all the skills needed to pick up the rest and get to the difficult part of picking up a new project: understanding the problem space. If you're capable of learning the meaning of an API you've never seen before - and you will, if you're not writing your last project verbatim - then you're capable of learning what a lambda means. This is not me saying that all programmers should be able to do this, it's me saying that doing this is a predicate of engineering software.

If your job involves maintenance or support of multiple Python codebases, you do have to learn it all. And at least before Python 3, this wasn't that hard. (Python 3, unfortunately, has made the language a lot more complex.)
True - some of the specific problems have been handled, but C++ is still kind of a disaster. The language has got even more complex in many ways. Some coders like that and get really into understanding the byzantine complexities, so the codebase inevitably gets dragged into unmaintainable, hard-to-debug cleverness. From my experience, C++ development is getting better, largely because tools are getting better at hand-holding and pointing out all the foot-shooting mistakes you can and do easily make on pretty much every line.

Maybe C++30 will have fixed everything...

Sure, "if" all of that old code (how many billions of lines of code?) was rewritten and redeployed since.

If not, then... the above statement does not stand.

Some of us still have to use outdated C++ compilers where the ::std namespace doesn't exist and all the modern goodies are but a dream.
Yikes, what platform is this?
I bet some nice embedded platforms making use of Embedded C++ dialect.

https://en.wikipedia.org/wiki/Embedded_C%2B%2B

C++11 was just around the corner then, in draft form. The article shows awareness of it.