Hacker News new | past | comments | ask | show | jobs | submit login
“Modern” C++ Lamentations (aras-p.info)
347 points by tpush on Dec 28, 2018 | hide | past | favorite | 249 comments



It's really crazy how much C++ has already evolved since the start of my career (with C++98). C++ has been the millennium falcon of languages; creaky and old, not pretty to look at, but still good where it counts.

That said, I completely switched over to rust many months ago and rarely look back. Rust is a tribute to C++, an incorporation of lessons learned that sheds all of the baggage C++ will never be rid of. Rust also has less of learning curve than C++ (although both are quite high), simply because C++ has so many odds and ends to keep track of these days.

Rust has also made me realize how bad class based OO is as an abstraction. OO glues together operator overloading, methods, inheritance, all into one package. Rust breaks those concepts into component parts that provide much better abstractions with less keywords and jargon to worry about.


I too started my career with C++ (and spent 10 years using it almost exclusively, before moving to JS some time back).

The first code block in the article made my jaw drop, and my first thought was "what the hell has happened to C++? I was only away for 5 years". Then I saw the "simple style" example and breathed a sigh of relief. That is the C/C++ I recognize. Even the third example with structs is not necessary. The author is right. Sometimes reusability is overrated. Duplication is better than the wrong abstraction.


Yea, but the struct example delivers on the idea of reusability (where the pythagorean triples algorithm is a stand in for some complicated algorithm) in any situation where this might apply.

It's a useful answer to the point: Well what if it was 15 for loops with a bunch of esoteric function calls. The struct example would still be better.


> It's really crazy how much C++ has already evolved

To borrow an old bit of snark: I don't know what the "default" programming language of 2025 will look like, but I know it will be called C++.

Like Ethernet (the original subject of that snark), C++ has evolved into a form unrecognizable to those who saw its birth. Unlike Ethernet, the older forms haven't gone away. They still reappear often in large/old/important codebases, keeping compiler writers busy and everyone else less productive. I'm generally the last person to suggest replacing things that already work, but C++ really does just need to get paved over.


OP has been proselytizing Rust on HN like it's the new coming of the Messiah.

This kind of language advertising where one can't even bother mentioning one negative thing about the product is sad but effective, because people want to believe that there's a perfect language out there...


> OP has been proselytizing Rust on HN like it's the new coming of the Messiah.

That doesn't even come close to being relevant. Address the argument, not the person.


Correcting the same misleading statements over and over again is a chat bot's job and not an enjoyable task for a human being. I was just pointing out that you're probably wasting your time, but you're of course free to discuss the finer points of class-based OO or Rust's learning curve for the Nth time, if you're so inclined.


My personal view:

C++ has issues and problems because it's too old and it's not going to get rid of the old baggage, hence it will have to carry it forward while adding new stuff on top (concepts, ranges...). Those issues are not going away, they will only get worse over time.

Rust has different kinds of issues and problems because it's still relatively young. For instance, pretty bad compile times (even compared to C++ with heavy templates use). But that's solvable, it will just take time. Most issues with the language itself that I've personally encountered were of the kind "it's not stabilized yet" or "it's not fully implemented yet".


I cut my teeth writing very C-ish C++98 for an embedded system in the '90s and the C++20 ranges example code from the blog might as well have been in an entirely different language. Sadly, I may have to stop listing C++ as a language I'm familiar with on my resume.


I write very C-ish C++17, that is, C + constexpr + a very restricted subset of templates (you can only use integral types as arguments). No template metaprogramming, ever. By extension you're also not allowed to use _Generic macro of the new C standard, if you absolutely absolutely must use it, then use C++ template. Metaprogramming with constexpr, static if etc is fair game. No class methods, just use C style POD with functions. You cannot use references (just pointers). You're not allowed to use any C++ library, but any C library is fair game.

This works really well in practice for us. We use GCC to compile.


After a foray into C++-ish C++ I also write fairly C-ish C++.

I see it as being like walking into an armoury and being able to choose anything you want. Chances are, if you want to resolve a domestic dispute, you don't want a claymore. Or a Howitzer. Or an aircraft carrier, or a lump of unshielded semi-critical plutonium. But that's OK, you don't have to choose them. You can walk right past those shelves to the shelf with the megaphone and the padded vest, and just take those.

That's how you program with C++.


> I see it as being like walking into an armoury and being able to choose anything you want. Chances are, if you want to resolve a domestic dispute, you don't want a claymore. Or a Howitzer. Or an aircraft carrier, or a lump of unshielded semi-critical plutonium. But that's OK, you don't have to choose them. You can walk right past those shelves to the shelf with the megaphone and the padded vest, and just take those.

Is this an Ammu-Nation ad? :)


Maybe my analogy would be clearer if I mentioned that half of the munitions in the stockpile were irrevocably pointed at my foot... but that which half is which is not clearly labeled and depends on subtleties of architecture and compiler. :)


I was with you until "you cannot use references." Why not?


Once you allow only pointers, you know if you see foo.bar, foo is a value on the stack. As a function parameter, you know that mutable calls with "." don't change state outside of the function.

It reduces mental overhead.


That's a good point. I use references, even with that mental overhead, because they're not nullable. Clearly there are trade-offs.


I think const ref is ok, but avoid non-const


I have nothing particularly against references, especially since they look nicer with function pointers. But, you can do everything with pointers alone and if you open the gates of hell.. erm I mean C++ then there will be countless questions about lvalue references, rvalue references whether it's ok to use T&& whether it's ok to use std::forward, std::move etc... It's just simpler to stick to pointers.


Is the codebase publicly available?


No, unfortunately I'm not at liberty to share this code.


I changed my resume to list "C++98" instead of "C++" a few years ago. But I could probably just leave it off entirely as I endeavor to write as little C++ as possible... JavaScript is getting the same way.


I'm not sure why anyone today would start a greenfield project in C++ instead of D, rust or Nim. Maybe in niches where C++ is deeply rooted, like games.


Because C++ is a stable, reliable and well supported (read: production-ready) language that works on many platforms.

Last I heard D was definitely not all of those (it doesn't even have a straight story on garbage collection, as its creator said himself! [1]), and I'm not sure about Rust or Nim.

[1] https://www.quora.com/Which-language-has-the-brightest-futur...


Rust is used in production by hundreds of companies, from ones as large as Facebook, Amazon, and Microsoft, down to tiny new startups.


The visual tooling is still very meh though. Just compare the kind of code completion you get for C++ (even something as gnarly as Boost) in modern IDEs, to what RLS can do in VSCode.


IntelliJ is heavily investing, and it’s already showing. We are too: https://ferrous-systems.com/blog/rust-analyzer-2019/

But yeah, if that matters to you, it’s less mature. We’ll get there!


As is C++ at all of those companies.


Sure. The question was if Rust was production ready, it has nothing directy to do with C++. Everyone knows C++ is production ready.


What is the state of UI bindings?


GTK is coming along really well. There’s two different native-to-Rust ones in progress I’m excited about. It’s not my area of specialty though.


I wouldn't say that d "doesn't have a straight story on garbage collection." It's solidly a garbage-collected language, and although there is interest in changing it, it's not there yet. D isn't entirely stable (features regularly get deprecated and then removed over the course of 5-10 versions), but why do you say it isn't reliable?


D's creator said himself (in the link I posted) that its garbage collection implementation is lackluster.

To clarify, I didn't necessarily mean that D is not reliable, just that it doesn't have the combination of stable+reliable+supported that C++ enjoys.


In many ways, it's about being the first to do something. I write a lot of code around DPDK for packet processing. It's a pure C library. I know rust has good C bindings, but I heard Go did as well. I started a project in go using cgo for the dpdk parts.

What a nightmare.

I spent more time figuring out the idiosyncrasies of cgo than actually writing useful code. And in many cases, cgo couldn't actually do what I needed to do. I had to write some functions and see inside of the .go file.

I'm sure rust would have been easier to call C with since the languages bare inherently more compatible, but I don't want to go down that path again right now.


The Go language has weak, C interoperability. The parents are strong in it. You wouldnt be going down same path. More like the opposite one that is closer to your actual goals.

I recommend Rust or D for your next try since compiler quality is best for those. D will be easiest to learn. You can always use unsafe in either if memory management gets in way.


What about cross platform with UI and requiring high performance? Eg anything for consumers with image processing.

Hard to beat C++ and Qt in this case, IMO.


For the UI rust has bindings for a lot of stuff and it's easy to generate more. There are also more and more native options. It's also very good for that use case because it makes certain high-performance things much easier/safer. Threading is a joy for example.

I've worked on an image processing project in C for a while and am now doing one in rust and would never want to go back. The code is much cleaner and easy to maintain. The zero-cost abstractions really pay off, and the safety guarantees means you don't spend so much of your time chasing heisenbugs from subtle threading and locking issues.


For the UI rust has bindings for a lot of stuff and it's easy to generate more.

I use Rust as my main language, but as far as I have seen gtk-rs is the only somewhat mature binding for an existing UI toolkit. This is not coincidental, since Gtk+ is a C (with objects) toolkit, it is much easier to bind than toolkits in other languages. Unfortunately, outside Linux, Gtk+ also looks pretty out of place.

AFAIK, there isn't any binding for e.g. Qt or Cocoa that has wide API coverage and is mature. Definitely not for production-level work. I think currently the only viable solution is to write a core in C++, expose C functions and write the UI in C++, Swift, etc.


I searched and saw some Qt bindings as well, but didn't check if they're complete. I've decided to just do 100% rust so I use conrod instead of bindings. But the cases where I've tried the GObject stuff it did indeed seem pretty well done, at least in the Gstreamer case. I'd expect that over time even this gets fixed and Qt bindings become first class as well.

> I think currently the only viable solution is to write a core in C++, expose C functions and write the UI in C++, Swift, etc.

This is the worst case and is not that bad. It does force you to write in two languages but on the other hand it enforces a cleaner UI vs backend split and that's not a bad habit to have.


I don't know about rust, but d has bindings to gtk and qt, in addition to its own hardware-accelerated ui library (dlangui), and nim can include c headers directly and so easily use gtk. Nim, rust, and D are all capable of c-level performance (numbers, pointer munging, etc. are all available the same as in c).


So you can take the same codebase with these and deploy a UI plus libraries to Linux, Windows, Mac, Android and iOS?

In a production ready manner?


(Note: this applies only to d as I don't know enough about the other two to say) not ios, but aside from that, yes.


For one thing, there are many more C++ programmers out thare than D or Rust. And nobody heard of Nim.


C++ is going to be around in 20 years. Maybe as legacy (although I doubt that), but there will be support for it even so.

It's not a given for Rust, and especially not a given for D or Nim (since they don't have any large companies behind them).


C++ is ubiquitous. Pretty much any modern unix on any CPU architecture — it's there. And it likes shared libraries.


Because "it's the language you know best" is a perfectly valid reason. Another valid reason is that, if used properly, C++ performance blows other languages out of the water.


Based on what I've seen, rust really can be just as fast.

D and Nim are a bit more iffy since they have built in GC. It can be turned off, but at that point you lose a major selling point over C++, memory safety.


Real world answer: no code is greenfield. You work in the established ecosystem.


I like Nim but its ffi, while really good, is not effortless. You have to wrap every function you call, which in large C libraries can be an incredible pain in the neck.

There are tools to help you out, like c2nim, but in my experience the output it produces requires fixing to get it working, and of course when the upstream library changes or adds to its API those sorts of things have to be taken into account as well.


I've never heard of Nim. Is this it: https://nim-lang.org? Why would you choose Nim over C++?


I've personally been watching Rust from the sidelines. I am still waiting for a language to tackle excessive compile and link times.


Nim compiles to C, so can at least make use of fast C compilers. But the solution really seems to be to just use a fully dynamic language like Common Lisp that can compile, load, and redefine (hotswap) everything incrementally so you're not having to start from nothing all the time.


Compiling to C is not a compile performance advantage. If anything it slows you down as described in the article (header files). It's just not as bad as C++.


I've not used it for anything too big, but D compiles incredibly fast.


On big projects, linking times are really the bottleneck.

Sure, incremental linkers exist [1], but all of them tend to do O(n) work on every invocation. Source control software, even developed explicitly to scale [2], behaves the same way. Makefiles as well; Tup [3] tries to solve this, in vain since the linking step is still holding everything up. There is so much inherent inability to scale built into our tools. So on big projects everything grinds to a halt as you cannot buy enough developers and hardware to keep up with O(n^2) forever.

[1] MSVC cl, GNU gold

[2] git: http://lkml.iu.edu/hypermail/linux/kernel/0504.0/2022.html

[3] tup: http://gittup.org/tup/manual.html


This can be solved by putting individual components into shared libraries.


But then you pay the cost every single time on startup, unless your components barely interact.


Recent article regarding D compile times: https://blog.thecybershadow.net/2018/11/18/d-compilation-is-...


It is very fast for certain projects, but link times on some can make up for that. I have a toy program using vibe.d that takes around 1s to compile and 10s to link. I could probably make that faster by switching to gold, but I didn't yet.


I'm not sure how anything over 100ms could be considered "fast compile" for a toy program... Basically Pascal, or simple c compilers like tcc should be the benchmark imnho.

That's not to say I don't allow for trade-offs.. I gladly trade some milliseconds (or, grudgingly seconds) for features.


Most of the compile time of that particular program is spent parsing HTML templates and converting them to D code. This is all done by D code provided by a library that runs at compile time as part of a template function instantiation in my code. So the compile time is inflated a bit in this case.


Object Pascal? I guess Ada also has pretty good compile times - but I'm not sure if the Pascal heritage stretches to modules/compilation.


I don't think Rust has anything comparable to implementation inheritance like C++ has? Getting implementation inheritance from pure composition is essentially a "tying the knot" exercise, where you must ensure that "virtual" method calls are always dispatched on through something like an existential type, so that they can reach the right implementation for the actual "child" object. (The right question to ask is "when do we actually need this, ffs?" Almost never, as it turns out - it's just needless complexity. Hence, "prefer composition over inheritance!")


The statement that I found most powerful:

> Compile time of this really simple example takes 2.85 seconds longer than the “simple C++” version.

> Lest you think that “under 3 seconds” is a short time – it’s absolutely not. In 3 seconds, a modern CPU can do a gajillion operations. For example, the time it takes for clang to compile a full actual database engine (SQLite) in Debug build, with all 220 thousand lines of code, is 0.9 seconds on my machine. In which world is it okay to compile a trivial 5-line example three times slower than a full database engine?!


In many ways the '++' in 'C++' comes from giving the compiler more information to work with. You can opt to trust the compiler much more than in C, and the idea is that you gain time at develop-time and hopefully run-time with the tradeoff of losing it at compile-time.

At least, that's how I've usually understood it. And the example you mention seems like a complaint with C++20's Ranges in particular. I guess I'd fall into the, "but can't you just ignore the parts you don't like?" camp.

This post is written in the context of game engines, but I see a lot of the same 'OG C' arguments made when people in embedded development resist giving C++ a try.

We're all writing resource-constrained applications that work on tight time budgets, and none of us feel comfortable throwing up our hands and 'trusting the compiler' in that sort of atmosphere. But C isn't getting any younger, and it's hard to do polymorphism without getting hack-y.

Now, who's turn is it to advocate Rust?


> At least, that's how I've usually understood it. And the example you mention seems like a complaint with C++20's Ranges in particular. I guess I'd fall into the, "but can't you just ignore the parts you don't like?" camp.

The issue is that many libraries (including std/stl) tend to produce long compile times.


> You can opt to trust the compiler much more than in C,

I feel that in modern C++ modern style ”trust the compiler” often means ”leave the programmer out of the loop”

I really, really would hate to have to try to debug the ranges example, for example.


Here's an implementation that is equivalent to the modern C++ example (playground link: https://play.rust-lang.org/?version=stable&mode=release&edit...):

  use std::time::SystemTime;

  fn main() -> Result<(), Box<dyn std::error::Error>> {
      let t0 = SystemTime::now();

      let triples = (1..).flat_map(|z|
          (1..=z).flat_map(move |x| 
              (x..=z).filter_map(move |y|
                  if x * x + y * y == z * z {
                      Some((x, y, z))
                  } else {
                      None
                  }
              )
          )
      );
    
      for triple in triples.take(100) {
          println!("{:?}", triple);
      }
    
      let elapsed = t0.elapsed()?;
    
      println!(
          "{}", 
          elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9
      );
    
      Ok(())
  }
It takes about .376s to compile in debug mode, about 0.491s in release mode. This was after warming caches; the first run took about 2.071s, of which a large portion was probably loading the compiler and standard library.

In debug mode, it runs in 0.047586s, in release mode, 0.002304.

I was pleasantly surprised to see this is actually better on the build times and debug build performance, given that those are two often cited sore points in Rust.

I would also say that while this is a little bit noisy, it is more readable than the proposed C++20 version. "1.." looks more like a range starting at 1 than "iota(1)" does. There's an actual "if" statement in there.

In fact, while Rust doesn't officially have generator/coroutine support yet, it is available in nightly builds for the implementation of async/await. If you use the nightly compiler, enable the unstable features, and wrap an adapter type around Generator that implements Iterator, you can write:

    let triples = IterGen(||
        for z in 1.. {
            for x in 1..=z {
                for y in x..=z {
                    if x*x + y*y == z*z {
                        yield (x, y, z)
                    }
                }
            }
        }
    );
It's expected that at some point in the coming year or so async/await, and probably the underlying Generator feature, will be stabilized, and I presume the standard library will implement the Iterator trait for appropriate types of Generators (though there is some bikeshedding about the details of that).

Rust doesn't have generators yet, so this is only really fair to compare to the generator/co_yield example in C++ (which is also proposed but not yet standardized). I'd say Rust's syntax compares favorably to that example; the ranges are more clear than the traditional C-style "for" loops, there's native tuple syntax, and it's just be "yield" instead of "co_yield".


Rust sore points regarding compile times have to do with lack of support for binary libraries.

While on C++ I can take advantage of incremental compilation and linking, while using binary libraries for all dependencies, with cargo I have always to build the whole world from scratch minus standard library.


I'm not sure when the last time you've used Rust is, but that hasn't been true in quite a while (not sure how long exactly). Cargo now has incremental compilation, and it's turned on by default. It's great!


I dabble on Rust regularly, usually during plane or train trips.

It still doesn't do incremental linking, and cargo still builds everything from scratch without any support for binary libraries.


Binary libraries don't help so much -- many libraries use generic types and functions (those are instantiated and compiled for each specific instance) which can only be delivered as library "metadata", not codegen'ed, and the latter is the very slow part.


Wrong since C++11 introduced external templates for common instatiation types.

Additionally, it is not like every C++ library is full of meta-programming templates.

Finally, there are other languages with library "metadata" and generic data types, which can easily beat Rust compile times like Delphi, Eiffel and Ada.


Delphi and Eiffel compilers don't do anywhere near the same level of optimization as LLVM does.

And are you sure GCC's Ada has better compile times? My sense is that so few people use Ada at scale that we don't really know.


Me! Sort of.

Rust is definitely further along the scale towards the end of "giving more to the compiler so it takes longer". I usually like the trade. Most of the time.


Rust also has a saner module system that makes incremental builds much quicker. They're still pretty slow, but not as agonizingly slow as a clean build.


Rust is only fast compared to C++ (and then only sometimes). Literally every other language is significantly faster to compile. I'm hoping this can change though. Rust with fast compile times and a more complete library ecosystem (and ideally better IDE support) would be an incredibly productive language.


Scala is slower to compile than C++ in large scale enterprise development. (The exception that proves the rule?)


Ah, I forgot about Scala. Someone below mentioned Haskell too (which I haven't used). Still, calling Rust fast seems wrong to me!


Compared with C++, Scala projects are not slow to compile when you use Gradle multi-project builds (with granular sub-projects) and Gradle build caching.


Not as agonizingly slow, but still agonizingly slow.

While I do appreciate build speed improvements, saying that it's better than it was isn't a super high bar in terms of user experience.

I love Rust, I really do, but it's still incredibly slow. I'm excited for any progress it makes on this front.


I think the most promising thing on this front is Cranelift (https://github.com/CraneStation/cranelift). The idea is that, when cranelift is ready, we might use it for debug builds (but still use LLVM for release builds).


I was an early user of the language in the 80s and early 90s (with for example cfront which compiled to C and later the original g++ and its libg++).

> In many ways the '++' in 'C++' comes from giving the compiler more information to work with.

This is definitely not the origin of the '++'. I believe the '++' name was literally a play about "one better than C."


I remember Turbo Pascal 5.0 and its imperceptible delays between pressing F5 and seeing the program output. I hope some day we can have this in C++, even if it takes sorcery like speculative compilation as-you-type done on a gigawatt cluster of custom chips...


Pascal (and the entire family - Modula etc) is a language designed by a guy with an academic background in PLs. This shows in the design: e.g. the syntax is verbose and has a very rigid structure, which can often be frustrating to the user (like the need to declare all variables in a separate declaration block at the beginning of each function), but which makes it extremely easy to parse with just a single token lookahead. Similarly, the semantics are such that they're easy for a compiler to implement.

Borland still did a great job with their implementation performance-wise, and the legendary reputation of their Pascal compilers is well-deserved. But they had a solid foundation to build on in form of a tool-friendly language, which C++ just doesn't offer.


Those rules are quite relaxed in Delphi, while still keeping the compilation speed.

Same applies to C++ Builder, at least until they decided to move to clang.


By now I think parsing is not exactly the bottleneck. But back in 90s, I'm sure that it has been a part of why TP and BP were as fast as they were, esp. compared to C++. Having precompiled units (and therefore no need to #include hundreds of kilobytes of headers for every translation unit) is probably a bigger deal today.

Also, IIRC Delphi had only recently gotten generics, and they're still not heavily used? I suspect that code that's heavy on those will compile pretty slow.


I have seen a modern Oberon compiler building itself in 1/10-th of a second. (The Tiny C compiler does not take much longer, either.)


It was one of Pascal main features in tape drive era: single pass compiler.


The size comparisons are also very useful, if anything to show that even the simple "non-Modern" C++ program already has quite a bit of bloat.

Compilation takes 0.064 seconds, produces 8480 byte executable

I am experienced with Asm so I can come up with an instinctive order-of-magnitude estimate, but even those who aren't should try to roughly estimate how many machine instructions are required to implement that code, and come to the conclusion that over 8000 bytes for three nested loops with not much in them, a comparison, some arithmetic, and a handful of library function calls seems rather excessive. Even 800 would be on the high side --- somewhere around 100-200 bytes is my estimate.

Also, watch this 4k demo (everything you see and hear is generated in realtime by a 4096-byte executable):

https://www.youtube.com/watch?v=jB0vBmiTr6o


The 8480 includes bytes used for names of external symbols, etc.

otool tells me the assembly routine is 74 instructions (279 bytes, if I’m counting correctly)


I don't think that this is a very meaningful comparison at these sizes, because it entirely depends on your target. Just compiling a "foo.c" file with "int main() { }" for sole content through "gcc -Os" produces a 16kb executable on my linux machine, but I know very well that the exact same compiler will produce something much smaller if I compile for, say, a bare metal AVR target because the ELF file format has a lot of fluff.


> because the ELF file format has a lot of fluff.

I wouldn't call it fluff: even assuming no debugging info (which you won't have in your avr object file) it has info needed at runtime (program start time) such as where to load what into memory, how much initial uninitialised memory to allocate when starting up, etc. It all gets used.


I think the keyword might be an 8k executable, not 8k of code. If I compile the example with clang on my box (linux x86_64) I get a 17K executable, for which the .text section (containing pretty much the runnable code)is 0x291 (661) bytes with the debug build and 0x255 (597) bytes with -O3.

If I dig into the .text segment of the optimized executable, only 235 bytes belong to the main function (others are stuff like __libc_csu_init and register_tm_clones). Looking at the assembly the loop would be about 123 bytes.

So lots of overhead, but I wouldn't say it is in the nested loop.


I abandoned C++ and Windows programming in 1996 to focus on hardware and firmware. So I'm rather familiar with just straight C and keeping code size down.

You have to be careful, a lot of those 8000 bytes might well be printf().


You have to be careful, a lot of those 8000 bytes might well be printf().

If it was a statically linked executable, it would be far bigger than 8K.


It's probably a few kilobytes of symbols, a few kilobytes of crt0, some ELF overhead, and a few hundred bytes of user code.



> Compile time of this really simple example takes 2.85 seconds longer than the “simple C++” version.

Haskell, Rust, Scala etc. also have a lot of trouble with compile times. I wonder why, though - is it just a matter of priorities being placed on things other than optimizing the compiler, or is there some actual, inherent complexity in compiling these "modern" languages that doesn't apply to C or Go? (I suspect mostly the former, FWIW).


With Rust, a lot of the issue is monomorphization. Changing one line may make hundreds of lines be needed to recompile. Additionally, we produce more LLVM IR than we must, and rely on LLVM ago boil it away.

It’s a complex set of issues that we’re constantly working on.

(So yes, Go and C do not have generics, and so do not have this problem.)


Stupid question from a small brained primate.

Would it make sense to have two types of generic implementation with a boxing unboxing mechanism. AKA a fast one that requires a rebuild when you monkey with something. And a slow one that doesn't but compiles fast.

I think it'd be acceptable if the 'dev' version used GC as long as the final build didn't. Basically anyway to get the development cycle to go fast.


Not stupid! You wouldn’t need a GC to do this either. It’s been talked about but nobody has tried to implement it.


Almost like combining Siek’s gradual types with parametric polymorphism: you can search for “gradual parametricity” and find a number of papers on the topic.


Rust actually has that they just can't be substituted automatically because they're different types:

    fn<T: MyTrait> compile_time(thing: &T) {}
    fn run_time(thing: &dyn MyTrait) {}
The reason that you can't just substitute them is that you can do more with compile-time generics than runtime ones -- runtime generics have unknown size (different concrete types have different size, so what size is the runtime generic version?) so they always have to be used through a pointer (and usually heap allocated). There are a variety of other restrictions on trait objects (runtime generics) for type-system reasons.

It would be possible to find cases where compile time polymorphism is replaced with runtime polymorphism, but I'm not sure it would really gain much given the restrictions.

Now if you start changing the language semantics a lot more becomes possible, but I don't even know what changes you would have to make to the language to let that happen.


That's when Jai (if it ever becomes real) hold promise: fast compilation is a first rate goal, not a second rate goal where you decide language features first then try to make the compiler "not too slow".


Do you know what's causing that?

1. Maybe monomorphisation overduplicates code, so that different invocations produce the same machine code in the end?

2. If that's not happening, maybe idiomatic Rust code causes a lot of code to be generated where a C programmer would have written something differently, e.g. using a void* instead of passing by value?

3. If that's not happening either, maybe monomorphisation is more or less producing what a C programmer would have monomorphised manually, but it takes the compiler a long time to optimise away abstractions?

It seems that in this C++ example the problem is #3: it takes a long time to optimise away the range & lambda abstraction.


It’s all three.


is monomorphization a more concise term for what is called "implicit template instantiation" in C++?

If so, is Rust worse than C++ when it comes to compile times of those? Heavily templated C++ code is known to be slow to compile due to this. Is it much worse in Rust? Are you aware whether C++ compilers do some special optimizations that Rust compiler doesn't have yet?

I don't see what can be done to make this better, the whole point of templates is to "pay" at compile time for some gains at runtime. (or in this case for gains in the amount of code a developer has to write)


“Monomorphization” is what happens when a template is instantiated. It’s essentially the same thing in both languages, and has roughly the same compiler performance implications.

There’s at least one thing that could be done, in Rust at least. But it’s only in some limited circumstances. See here: http://www.suspectsemantics.com/blog/2016/12/03/monomorphiza...


The compiler could do that automatically. Suppose you're operating on the control flow graph and you are to specialise a basic block in some type environment. You can cache the result so that the next time the same basic block is specialised in the same type environment you look up the result in the cache and create a call to that existing basic block.

  pub fn big_function<T: Into<i32>>(x: T) {
      let y: i32 = x.into();
      ... code that uses y but not x ....
  }
In the first line of the function you have the environment {T: Into<i32>, x: T}. After the let you have the environment {T: Into<i32>, x: T, y: i32}. If you keyed the cache on the full type environment you wouldn't solve the issue because the code that only uses y would still get specialised to the {T: Into<i32>, x: T} too. However, you could detect that that code doesn't actually use T and x, so that you can specialise it to the type environment {y: i32} only.

That detection can happen as a side effect of specialising that code to some particular {T: Into<i32>, x: T, y: i32} for the first time. As you specialise that code you record which parts of the type environment actually got used, and you use only those parts as a key in the cache. The type environment object itself could take care of recording what the compiler looked up in it. Another advantage of doing it this way, rather than analysing the code ahead of time, is that it can handle cases where a particular type variable does or doesn't get used depending on what type some other type variable is instantiated to. You could also use the same system to avoid duplicating code that only relies on particular aspects of a type. For instance, a function that permutes the values in a &mut[T] might only care about the size of T and not about the precise type T, so that all its specialisations to T of 4 byte size can call into the same code.

Another thing you'd probably want to do is integrate a basic form of constant propagation & dead code elimination during monomorphisation, so that you don't spend a lot of time monomorphising code that ends up dead for particular type instantiations.


> The compiler could do that automatically

Yes, that's what I'm saying.

> Another thing you'd probably want to do

This is a very interesting idea!


The translation units in C++ tend to pay this cost a number of times over because of the way header files are processed. With modules, this theoretically could start getting closer to Rust, but for large projects, C++ templates can still be orders of magnitude worse in terms of instantiation cost.

This is partly why there are hacks like unity builds (not related to the engine, where all source is bundled into a single translation unit). These have plenty of drawbacks too so it's not a clear win.

Adding to all of this, there are fancier mechanisms for template meta-programming like SFINAE rules + computed template values. Sure, it's "turing complete" but this is why we see such clever libraries with huge explosions in code generation size. I'm far from an expert in modern C++ features but it's clear that there is an entire interpreted programming language of templates bolted on the to the rest of C++. It reminds me of this post on a similar take on Haskell type level programming: https://aphyr.com/posts/342-typing-the-technical-interview (or similar feats by Oleg Kiselyov).


Thanks, that's an interesting read. indeed a developer can use that trick "in the case of conversion traits".

Monomorphization seems to be the source of 2 major problems: compile time, and binary size.

An optimization that could reduce the compile time by caching the generated code (at least when compiling over-and-over the same code base. i.e. "Incremental compilation") - and it seems that Rust already is doing something with it [1]. I wonder if something specific is done for template instantiations there.

Optimizations aimed at reducing binary size seems much more tricky, if not impossible, except as you pointed out in the limited cases described above. In the regular cases, templates are kind of working as intended: the developer has to think if he/she would have written the same amount of code N times if templates were non existent.

[1] https://blog.rust-lang.org/2016/09/08/incremental.html


C does have a kind of lightweight generics since C11.


Sadly C’s `_Generic` typecase mechanism does not do SFINAE: all the branches have to typecheck OK, regardless of which one is selected. This is OK for <tgmath.h> where the cases are different sizes of float, where there are few opportunities to get different failures in the different cases, but it makes _Generic hard to use for more template-like code.


Given how hard SFINAE is to understand for typical developers, I consider it a good thing, actually.


Sure, but I’m not aware of them being used in any significant capacity, so I don’t think it’s particilary pertinent to this discussion.


Ah, in that regard you are right, I guess.


My feeling is that at least in C++ and Haskell, what's going on is that a barebones language core is being extended with a teetering tower of mutually dependent libraries building ever deeper abstractions. Templates upon templates, or monads upon monads.

Whereas in Go, say, stuff like ranges and maps is just built in, no more abstraction than is necessary to do a generalist job with a few specialised common cases. Even the standard library is largely one-level, direct code, and only abstract at the interfaces.

So simply and literally, C++ has to compile more lines of code to do the thing.


This was of course one of the key differentiators of C; one of my favorite lines in the original K&R book was "what? I have to call a function to do I/O?"

C was unusual that so much of it was in its libraries rather than being built in. Programming language design in those days didn't have the clean differentiation we have today.. or perhaps did: it's fascinating to see the arc bend back.


Being in libraries or in the language isn't so much the thing, because really the language is just calling into a hidden library. It's more being directly written, versus being composed of Legos that are themselves composed of Legos... that ends up drawing in a hundred thousand lines of ultra generalist code and then pre-specializing it.


It predates C tho. In Simula-67, not only I/O was in functions, it was in methods (on file objects). In Pascal, it was also all functions, also some of those were magic (e.g. overloaded on types, in a way user-defined functions couldn't be) - but it still described them as functions.


I said unusual not unique, but perhaps I should have been clearer.

Pascal and C were definitely influenced by Simula, a language ahead of its time.

When K&R wrote that line I suspect they were thinking more of PL/1 which was the Multics implementation language. Though it has OPEN/CLOSE etc they were keywords not external labels.

I think it’s pretty likely K had heard of Pascal at the time.

More interestingly BCPL’s i/o was via library rather than innate. And c descends directly from BCPL (->b->c) rather than from PL/1; simula-67 and BCPL were developed contemporaneously in Europe.

Again, apologies for my prior brevity.


Fast compilation was an explicit design goal for Go: https://talks.golang.org/2012/splash.article#TOC_4.


It must be different ways people work. I can't imagine caring that my integration test suite might get 0.05 minutes slower.

Edit: especially if that 3 s pays for generating code that nobody has to write correctly or ever look at.


Isn't it more about what the typical program looks like? The generic languages like Rust and C++ generate a ton of functions, wrapper types, and indirections that need to be seen through and mostly removed by the compiler.

In C++, when compiling:

   for (; *it; ++it) { }
it may look simple but deref is a function you insert (deref operator), `++it` is a function and so it continues on, with new function to generate each time it's a different iterator or even the same iterator with different template parameters.


It also does not apply to .NET Native, Delphi, Ada, Eiffel for example.

On Haskell's case it depends on which compiler backend you are actually using.

So yes, it is a matter of effort spent in the code compilation, specially if the language does support modules.


And that is a really old problem. I remember trying boost::phoenix about 10-15 years ago but it was completely unusable because even the hello-world-example took like 10 seconds. Thats incompatible to my C++-workflow.

Today I'm trying to port some big old Embarcadero (Borland) C++ projects to their new clang-compiler but the compile times are so terrible that I'm not sure this will be usable at all (and these projects are mostly C++98).


Well, it doesn't help that Borland focused on compiler performance (i have Borland C++ 5.0 and i use it for a lot of my C coding since i write in C89+a few extensions that are supported anyway, because it takes around a second for a full rebuild for my code and is practically instant for a partial build - and that is for a singlethreaded compiler, for comparison the same code takes 4 seconds on a parallel build in gcc with make -j8). I don't know of any other (non-toy) compiler that focused on that. 15-16 years ago i used the free BC5.5 command line tools exclusively because on the crappy computer i had at the time (Pentium MMX at 200MHz with 32MB of RAM) it was the only that was fast enough to work with.

I've also played a lot with Borland C++ Builder and the compile times for C++ (pre-C++98) are comparable (that is, practically instant).


> In which world is it okay to compile a trivial 5-line example three times slower than a full database engine

Well, as explained in the article, those 5 lines generate hundred thousands of lines of code behind the scene.

And you don't use C++ for fun, you use it because you have a serious performance problem. And in that case you'll wait the compile times, because there is no alternative.

Let's not even go into link time code generation or profile guided optimizations, which increase compilation times by another order of magnitude.


> those 5 lines generate hundred thousands of lines of code behind the scene.

That's part of the problem; most of the time, the "thousands of lines of code" that are created from type-generic code have a lot of useless duplication, and the compiler has trouble coalescing the duplicated and redundant code (I don't even think a serious attempt is made, AIUI). There's a lot of scope for improvement.


There is an alternative: writing simple C++ instead of Modern C++, or even using plain C.


Then you increase the time you need to spend writing your program. You will also most likely end up cutting some corners, and the result will be of questionable quality in one way or another.


Holding in my brain and using all these "modern" features increases the time I need to think about code, data, and algorithms, and that's usually where most coding spends my time -- thinking.

And "modern" compile times... increasing the time between results and further thinking and writing. Don't forget that; the article doesn't.

And "modern" debugging... well, good luck.


Well, people have been known to hold in their brains information pertinent to their trade. Professional training helps, too. I can only guess the extent of what a mathematician, a physicist or a medical doctor must remember to be successful in what they do.


Musicians are able to hold complete compositions on their heads, some of them several minutes long.

It is all a matter of focus and pratice.


Code is not music. When thinking about code you must keep track of possible branches executed code might take and properties of the physical machine it runs on. C++'s complexity can hide code's underlying logic and make some types of bugs more common.


As someone that learned music and several instruments before getting into computing, and having spent several evenings with musician friends, I beg to differ.

Writing a composition score for an orchestra falls under similar conditions, how each instrument sounds, how they sound together, when each comes and goes into play, and so forth.

Most complaints about C++'s complexity can be equally applied to languages that look deceivably simple like Python.

Complexity is like thermodynamics, it gets pushed somewhere, if it isn't on the code, it gets shoved either into boilerplate, or architecture.


If we can compare C++'s complexity to Python's, then C++ has lost its way. You're saying the same thing as the article and my comment, that C++ is trying to hide complexities, and we're arguing that doing so is instead adding more complexities.

Sure, Python makes it easy to listen on a socket, parse HTTP, and send HTML down -- hiding a lot of complexity. But it's made it impossible to do so very efficiently and scale up. Try and dive into Python's stack for that (Re: 'hold in your brain all the "modern" features'), and you'll get lost in a myriad of CPython (or whatever flavor you choose) generics and indirection that you can't optimize. Startup times matter, because you are all about hiding complexities and went "serverless" and constantly eat cold starts (Re: '"modern" compile times').

I think it's apt to compare "modern" C++ as moving towards something like Python. Not great for a lot of applications of C++ (Re: games, in the article and my focus) or its perceived goals as a better C. That's the point. Many of the C++ additions are being slagged for making a worse C. It's looking nothing like C any longer. It's a giant ball of complexity with dozens of ways of doing the same thing and hoping you learned them all.

This is why I think languages like Rust (or even Go) get a lot of attention. They have a good interop story with C, like C++ has. And if we're going to learn a bunch of non-C looking language grammar, C++ is doing a worse job.


My point about Python is that it is always presented as begginer friendly language, yet is it quite complex for anyone that whats to fully master it.

The language + standard library + C API references are around 2392 pages, not counting the other minor documentation like the 500 PEPs, release notes and differences across each Python release, even minor ones.

Then there is the whole set of meta-classes, decorators, mixins, generators, operator overloading, multiple inheritance, abstract classes, fp like programming and much more.

The big difference to C++, is that the community doesn't care about performance, leaving the PyPy guys a Quixotic battle regarding adoption, which isn't the case with C++ compilers.

Even C isn't as simple as many think, with its 200+ documentated cases of UB, and the days that C mapped 1:1 to PDP 11 Assembly are long gone.

How C code looks like, and what gets generated via auto-vectorization, GPGPU code, SIMD intrisics are very much two worlds appart.


You're absolutely right that Python has also become more and more complex and often not for the best.

As for Python performance, on the contrary, I think there was a large portion of the Python community that did care during my years with it. I think 3.x adoption was hurt a lot by being slower than 2.7.x. I think a lot of the community jumped ship to Golang or similar, for both performance and complexity reasons.

I would absolutely love for C++ to tackle C UB or make incompatible insecure C. Instead, we get stuff like Ranges, time and again. I'm struggling to understand what complexities you are finding it covers that are worth supporting forever, committing gray matter, committing productivity loss, committing debug travesties, &c. Say I'm a musician that can hold complete compositions in my head; why should I commit this feature and its baggage to memory?


> Say I'm a musician that can hold complete compositions in my head; why should I commit this feature and its baggage to memory?

Because C++ is the sane alternative to C on OS SDKs, HPC, GPGPU programming, regarding portable code.

Now, ideally as memory safe systems advocate, I would like to see more of Swift, .NET Native, D, Go, Rust, Java (AOT), OCaml, ...

However, when using languages not endorsed by vendors, it always means inferior IDE/debugging experience, lacking IDE support, manually writting FFI integration code, tracking down which layer is responsible for certain bugs,..., so switching to something that looks easier turns out to become harder in the long run as the whole stack experience suffers.

There are already signs of OS vendors finally improving a bit the situation, with Apple continuously pushing for Swift, Google restricting what is at all possible to do with NDK, Windows teams focusing on .NET Native in spite of C++/WinRT (nee C++/CX), Rust adoption by all big names.

However we are still at least a decade away of any of those languages enjoying a similar industry position in platform SDKs as C++ enjoys nowadays, after 30 years fighting against C.

So I rather keep that baggage in memory, while doing my best to use modern C++, alongside the other more type safe languages, also part of the respective platform SDKs, than additing additional attrition to my production code toolbox.

I imagine my opinion is not unique.


As someone who also learned music and played several instruments (violin, piano) before getting into computing I still don't get what's the point of comparing musical memory to being able to recall programming language rules. Programming is largely about managing complexity and our mammal brains are not very good at keeping track of all the exceptions and special cases a language like C++ is full of (as opposed to remembering music). Just to be clear: Yes, writing music, especially for an orchestra, IS hard and complex.


By simple you mean without the STL?


C with polymorphism


Including virtual/dynamic polymorphism?


If you really have a serious performance problem, the answer these days is more likely to be a GPU, DSP, FPGA, or even an ASIC. The era of C++ on CPUs being the right solution for most high performance computation problems is coming to a close now that Moore's Law is ending.


If you deploy the code on a large number of machines, especially out of your control, most of those don't apply. There is a large scale of problems that require/benefit from optimised non-GPU code: - OS and related tools - applications (e.g. image processing, spreadsheets, engineering applications) - browsers - games - compilers - low level libraries - databases

Yyou are probably using many of those and would not enjoy them being slower.


Even though I do like C++, I kind of agree.

As Wirth languages fanboy, I belive that if Java 1.0 had AOT compilation from day one, instead of leaving it to commercial third parties, and proper value types, the amount of C and C++'s adoption would have looked much different.


I wonder what Wirth would have to say about Java...


As you might imagine, not kind words.

"Modern languages like Java and C# may well be superior to old ones like Fortran, PL/I, and C, but they are far from perfect, and they could be much better. Their manuals of several hundred pages are an unmistakable symptom of their inadequacy. Engineers in industry, however, are rarely free from constraints. They supposedly they must be compatible with the rest of the world, and to deviate from established standards might be fatal."

https://www.inf.ethz.ch/personal/wirth/Miscellaneous/IEEE-An...

"In 1995 Sun Microsystems presented its language Java , fully 6 years after Oberon. It incorporated much of the "philosophy" of Oberon, but, alas, chose the style and syntax of C. Around 2000 Microsoft released its language C# as a strong competitor of Java, and Google followed in 2007 with its language Go, even more strongly following (the 18 years old) Oberon. The crux with these languages, which all became wide-spread due to strong industrial support, is their size and complexity. The ambition to provide everything to everybody prevailed and let them grow into complex bodies difficult to master."

https://www.inf.ethz.ch/personal/wirth/Miscellaneous/Compute...


Using complexity as an argument against Go is not really accurate in my point of view.


It is, because after Modula-2, Wirth's goals have been to produce the minimalist, type safe, systems programming language.

Oberon-2, Concurrent Pascal, Active Oberon and Zonnon variants were designed by students or together with ETHZ fellow researchers.

Oberon is smaller than Modula-2, and Oberon-07 lost features on each language revision.

So from our point of view Go isn't complex.

From my point of view Go still lacks several features, which Go 2.0 will kind of address.

However, from Wirth's point of view, which after being retired was still trying to design such minimal language, Go is indeed complex.

You can see this by reading the language reports.

https://www.inf.ethz.ch/personal/wirth/Oberon/index.html


This shouldn't have been downvoted.


I totally disagree, I think that era has been over for the last 20 years.

I keep coming back to a talk by Daniel J. Bernstein, that the assumptions computer science teaches about performance are wrong. His claim is for real programs most code is ice cold and a tiny percentage is hot. And that's getting worse not better as time goes on.

What important is; correctness, latency, compile time. Not raw speed.


In my experience that claim is very true before you fire up your profiler. But once you've optimized your program for a bit the profile gets a lot flatter.


I think if your programs profile is flat it means none of your code is actually in the hot path. It's buried behind library functions and OS calls.


I think that at this point, C++ has basically to be treated the same as the English language. Sure, you could write sentences that span three pages, employ Shakespearean vocabulary or words that hardly anyone has known since the abolition of Classics from university admissions tests, or encode another layer of meaning in the bit pattern of when you do and when you don't use Oxford commas. What makes something good English communication is not whether you used those features correctly, employed the most in vogue ones or found the "most elegant abstraction" ("there's this one word attested in a handful of 14th century sources that captures exactly what I want to say, so I'll use it without explanation"), but whether you have struck the right tradeoff of minimising the combined effort put in by you and your audience and conveying your message faithfully.

Of course, for English, this is exactly the sort of problem that is addressed by style guides (and teachers/peers reacting to you, and natural selection). I think what we need is more and better opinionated C++ style guides that don't just talk about what line to place your { on, but have the confidence to say things on the level of "std::move may add a marginal amount of efficiency, but greatly detracts from the legibility of code. Avoid using it altogether."


There should probably be two C++ styleguides: one for library authors and one for library users. For the former, use of std::move can be absolutely essential to abstractions that are both efficient and correct. For the latter, it probably means you're overthinking things.


This is a fair point, but I still feel like there should be some regulations on how it is used in libraries (lest we wind up in a "this chunk of code is going into a .so, so everything goes!" situation), since library code still has to be read and maintained.

Compare inline assembly: it can be absolutely essential to talk to hardware or use specialised processor features (like if you want to use fancy SIMD instructions in the inner loop of a linear algebra library), but everyone would probably agree that a library which keeps the actual assembler code contained inside one or two specific functions is somehow preferable to one where each of the 40 different ways of calling it happily start out with an __asm block that packs their respective arguments into the relevant special-purpose registers and only then call the common "do the actual processing" function.


Personal opinions on the "right subset" of C++:

* Yes to classes.

* Yes to inheritance and virtual member functions, because interfaces are a very useful and easy to understand abstraction. Moreover, especially for games, idioms such as "class Monster : public DamageableObject" are really the most natural thing. However, avoid situations in which you need dynamic_cast like the plague. No to virtual inheritance.

 - I really wish class memory layout were standardised more, but at least some non-standard assumptions are fairly safe in my experience: e.g. class A : B {...} ... B * x; assert((long)static_cast<A* >(x) == (long)x);

* Before you start using "friend class", consider whether you could get around it somehow. If a class is not public-facing, set everything public and enforce encapsulation by self-discipline rather than language features.

* Yes to templates in their C++ 101 application of making a $type-specialised version of classes and functions. No to using them to perform any real compile-time computation, or anything of the type Boost pulls. No to Boost.

* "Keep the parts of your code that only do C stuff in C".

 - printf over iostream.

 - (de)allocate classes with new/delete, but POD with malloc/free

 - exception: std::string over char* , because it is really that much more convenient

* Yes to lambdas, but only pass them as function arguments if there is no good alternative, because of legibility. Avoid stuff like the for_each(..., []{ ... }) in the Ranges example.

* Yes to STL containers and algorithms, because everyone knows how they work and they are usually good enough.

* Range-based for over for(std::container::iterator i=...). Using the Ranges TS when a plain for loop would do strikes me as novelty-chasing and violates "do C stuff in C".

* Cautious yes to auto. It does somewhat detract from comprehension in some use cases. Don't just auto everything because you are too lazy to figure out the type.

* std::shared_ptr only when you actually think you can't answer the question of what the appropriate point to deallocate a piece of memory is, or at least not without significantly changing the structure of your code.

* Avoid std::tuple, std::variant and other awkward STL attempts to replicate functional language idiom without the syntactic support. It's terrible to read. Unfortunately, STL containers sometimes force you to use pair.

* No to exceptions; their interaction with other language features is awkward, and the eternal difficulties with platform support make me feel no confidence in the reliability of the feature. (I'm unfamiliar with the bowels of the implementation, but it seems like unwinding C++ stacks is fundamentally a hard problem.)

* No to novelty features such as user-defined literals.

* goto only to break out of multiple levels of nested loops.

* Coroutines strike me as something that's unambiguously cool but so far removed from the standard idiom of C++ programming that I'm inclined to say no. Maybe there's an alternative style guide you could write that says yes to coroutines and no to some of the things above. Multi-paradigm should not mean "use all the paradigms".

(I'm very happy to be persuaded otherwise on any of these points.)


I have been exceptionally surprised and thankful for the coroutine implementation in C++ and its early adoption in Visual C++ compiler (MSVC). UI and I/O code (particularly for Windows) is much easier to both write and comprehend. The stack traces are clear and error handling is simple.

I have professionally worked in many async (and synchronous) languages, and the implementation of async in modern C++ is my favorite. That is weird to type out, but it's true. I'm really enjoying modern C++, but I do follow almost all the rules you list.


No std::unique_ptr?


I haven't encountered any context in which it would add value (i.e. you can't just figure out yourself where it will go out of scope and delete at that point, which also makes the code clearer), and all other things equal, using it hides the semantically relevant type (the interesting part of a unique_ptr<A> is the A, not the unique_ptr!) and makes code and error messages harder to read.


It feels like that goes with your no exceptions policy. If you use exceptions (which is a different argument), then the return point is not necessarily clear. A unique_ptr makes sure that the object is correctly deleted, even in the case of error conditions.

The compiler knows where the lifetime ends. Why do the computer's work for it?


unique_ptr is super useful for documenting the point in the code that owns a given object, and for providing some compile time protection against multiple deletions. (If you never write delete and only use unique_ptr, you have to do a relatively foolish thing to get double deletions.)


I've written some heap-heavy code and never used unique_ptr, but I don't remember ever causing a double deletion. What's a pattern which you figure makes one prone to doing that? (On that basis, I'm a little inclined to suspect that it may be more of an issue for programmers who came from a deeply memory-managed language like Java and therefore don't build mental models to keep track until when a given allocation is needed.)

(I have of course produced my share of memory leaks - praise be to valgrind - but they were all in scenarios where unique_ptr would have been too restrictive (without a Rust-style complete reconsideration of the code's ownership structure).)


One thing i like about using std::unique_ptr now that we have and use std::move() is to annotate in the API when you want to own the heap object being passed or to say that the api consumer should own the reference.

I get that if you just use it for usual the owning class, it doesnt provide that much, but it annotates lifetime, and its pretty cool when you get used to it.

if you see this:

    class X {
     Y* y;
    };
Does X owns Y? or is owned by Z and X is just using it?

    class X {
     std::unique_ptr<Y> y;
    };
Now you know for sure X owns y.

    class X {
      std::unique_ptr<Y> CreateY();
      Y* CreateY();
    };
Look how in the first example you know that X wont retain a copy of Y, and the caller will be considered the owner of the heap..

Now in the second example you are not sure if X retain and will handle the deletion of Y, and you should use Y, while in the first example its perfectly clear the api intention.

the same here:

    void AddY(std::unique_ptr<Y> y)
    void AddY(Y* y)
in the first you are aware you are passing the ownership of Y, in the second you wont be sure if you still need to handle the deletion yourself.

Before std::move() i get it, but after you can pass things by moving them, i dont get it why anyone would not like to use this.

For me this is basically the "lifetime annotation" feature, only that it is by convention, and not enforced by the compiler. Unlike others these are the reasons why i dont feel the urge to jump the Rust bandwagon, and prefer to use things like Swift when i need a more "chill" environment to work, as i didnt feel more productive in Rust than in C++, while with Swift it happened and it also has a pretty good story perfomance wise.

I prefer to mix C++/Swift as my perfect duo, than try to make it all fit in one language, as this always lead to frustation and a lot of headaches.


If code is completely under your control, then it's okay. But from my experience, these kinds of errors frequently stem from large code bases without a clear ownership. Think about a case that hundreds of people are working on the same code base. You may not understand 99% of code, and probably don't even want to understand all the horrors written by other programmers.

In fact, I have seen a number of horrific instances that someone decided to write clever tricks with object lifetime which leads to production crash and no one really understand (or even bother) what's going on there. The only way to deal with such code is looking into its implementation, probably for a week or two. And found that the original author has left the company.

In these cases, static type annotation helps a lot by serving as a formal contract. To reason about object lifetime, you don't need to dig into the implementation; all you need is looking into variable's type signature. Same thing can be applied to types other than those related to object lifetime.


Some people are robots and never write errors. I and most C++ programmers are not in that category.


You've never found a context in which unique_ptr is useful? I find this surprising. It basically removes a class of memory management bugs with heap allocated objects at a stroke.


> However, avoid situations in which you need dynamic_cast like the plague.

You need dynamic_cast for dynamic interface queries ("does this object support X?"), even in the absence of virtual inheritance.

Some would say that there's no downside to always using it when you can. If what you're doing is an upcast, it's a no-op. If it's a downcast, then at least you get an exception instead of an invalid pointer that may or may not blow up if what you're casting is not of the correct type.

> Before you start using "friend class", consider whether you could get around it somehow. If a class is not public-facing, set everything public and enforce encapsulation by self-discipline rather than language features.

Why? It's a language feature that's there to help you enforce said discipline. What's the downside?

> I really wish class memory layout were standardised more, but at least some non-standard assumptions are fairly safe in my experience: e.g. class A : B {...} ... B * x; assert((long)static_cast<A* >(x) == (long)x);

That the object is allocated at the same address as its base subobject is guaranteed by the standard, if I remember correctly. Your particular assert is not, though, because long is not guaranteed to fit a pointer (and doesn't, on Win64).

> printf over iostream.

Not only you lose type safety, but it's extremely easy to write code that kinda sorta works but actually doesn't. The most common case is passing a struct with a single field to printf - this is actually implementation-defined, and some implementations detect it and treat it as invalid, while others just put the contents of the struct on stack. So it happens sometimes that people e.g. pass std::string that way, expecting it to work with %s - and it does... on that one implementation, and for that particular string. Then it breaks elsewhere.

iostream is still bad tho, both design-wise and perf-wise. The answer is actually Boost, which has proper typesafe formatting.

> (de)allocate classes with new/delete, but POD with malloc/free

There's no benefit to using malloc/free whatsoever, and it forces you to cast.

> Avoid std::tuple, std::variant and other awkward STL attempts to replicate functional language idiom without the syntactic support

Tuple has syntactic support as of C++17:

https://en.cppreference.com/w/cpp/language/structured_bindin...

> No to exceptions; their interaction with other language features is awkward, and the eternal difficulties with platform support make me feel no confidence in the reliability of the feature.

If you ditch exceptions, you might as well ditch constructors as well, since there's no way to report errors from them otherwise. You're also forced to use new(nothrow) then. Basically, the language is designed around exceptions, and excluding them is fighting it.

But I don't see the point. C++ exceptions have been supported and stable on all platforms for a long time now. On sane platforms (i.e. not x86, but e.g. amd64), they're also zero-time-cost at runtime for the non-exceptional path. It's not the 90s anymore.


> But I don't see the point. C++ exceptions have been supported and stable on all platforms for a long time now.

How come we don't see exceptions being used in serious C++ projects then (e.g. Chromium and Windows)?


Either legacy codebases that have rules dating back to the times when avoiding exceptions actually made sense, or else a cargo cult.


> You need dynamic_cast for dynamic interface queries ("does this object support X?"), even in the absence of virtual inheritance.

I prefer using C-style casts for brevity, and implementing support queries like that explicitly (i.e. via a "type" member or GetType() function or whatever in the base class). Quite often, you wind up needing that information anyway (resulting in redundancy), and the C++ casts are extremely verbose and ugly.

> Why? It's a language feature that's there to help you enforce said discipline. What's the downside?

I've tried working with it, but in my experience this language feature very quickly consumes more cognitive energy (by having to maintain the list of friend classes and just having to deal with it being there, consuming space in the source) than it saves over maintaining discipline manually. (This maybe a consequence of me not having worked on a lot of C++ projects with a high (number of contributors)/(lines of code) ratio.)

> That the object is allocated at the same address as its base subobject is guaranteed by the standard, if I remember correctly.

Huh. All sources I could find seem to claim that it actually isn't, but maybe I'm looking at the wrong ones.

> Your particular assert is not, though, because long is not guaranteed to fit a pointer (and doesn't, on Win64).

My bad. I've mostly only used unixoid systems for too long now.

> printf over iostream

But printf is so much more legible when you have to format complex strings.

> passing struct or std::string to printf

Who does that?

> There's no benefit to using malloc/free whatsoever, and it forces you to cast.

I do value keeping portions of code that don't use C++ features in C for reusability purposes, and moreover there is the whole issue that it's easy to get confused about initialisation with int* a=new int (uninitialised)/new int() (=0). If you use malloc, it's clear to everyone that it's uninitialised.

> Tuple has syntactic support as of C++17:

Yes, ugly, non-principled syntactic support.

  std::tuple<int,int> a = {1,2}; // { }, like most other instances of lists of things
  auto [x,y] = a; // [ ], for some reason (not used for lists/tuples anywhere else)
  auto [z,w] = {1,2}; // this doesn't work at all
Surely being able to abbreviate B=C; A=B; into A=C; is a sane thing to expect when there is no good reason against it.

> If you ditch exceptions, you might as well ditch constructors as well, since there's no way to report errors from them otherwise. You're also forced to use new(nothrow) then. Basically, the language is designed around exceptions, and excluding them is fighting it.

I've worked with a number of embedded platforms that had C++ support without exceptions. As far as I know, there are also still issues when exceptions are thrown through non-C++ stack frames (e.g. a callback invoked from a C library). I'm aware of the problem of errors in constructors, but think that in general it is best to try and write code so that constructors don't do anything that can cause errors (I strongly prefer constructors to only serve the role of putting the object in a coherent state, and having a separate Init function for any nontrivial operations that could fail.)

Memory allocation may be an exception to this, but I feel like code where the sane reaction to OOM is not to treat it as a panic-abort condition is special enough that special measures such as an provisioning an explicit "constructed, but no memory has been acquired" state in your objects are justified.

I really do want exceptions to work - having worked with Standard ML for a long time, I'm well aware of how right they feel as a solution to error handling - but the C++ implementation of them still feels like it ultimately has way too many moving parts to be trustworthy.

(Edit: The new * italics syntax on HN is really throwing me off.)


> I prefer using C-style casts for brevity,

There are strong reasons to avoid them altogether in C++, not the least is that you can sometimes get a reinterpret_cast where you expected a static_cast semantically - usually because you have forgotten to include the header that has the definition of the type, and are dealing with an incomplete type (explicit static_cast will fail in that case; but C-style cast will be treated as a reinterpret_cast, even if it would have been a static_cast if the definition was visible and type was complete).

> Who does that?

The typical example is when people pass std::strings to printf, either because they expect them to work with %s, or because they simply forgot to call data(). Unfortunately, on compilers that don't try to detect structs-as-varargs, this often works by accident.


Excellent article! I stopped using C++ completely after completing the meme that is posted near the end, I wrote gazillions of lines of C++ over 20 years, used all the gizmos, and then, gradually started to cut down on them, eventually settling on stuff like --no-rtti and --no-exceptions.

It's mostly working on the linux kernel and qemu that showed me I could write extremely nicely structured code, in plain C. Very easy to debug, very readable, very quick to compile and run.

And then one day I looked at one of my C++ module and told myself I actually didn't /need/ all that jazz of classes, namespaces, constructors etc etc. It could be done as a small .h/.c couple with 2 functions...

And that day I started to convert many many lines of C++ to plain C. Because it's /indestructible/, it'll continue compiling and working on /anything/ forever, without having to figure out why the new C++ compiler is throwing you a 10 lines error message just because that codebase is 10yo and isn't up to scratch with the 'new' way of doing things.

I DO miss bits of C++, I miss some of the encapsulation it provides, I miss stack-based objects, and funnily enough, I miss exceptions. But I haven't looked back.

The other points he makes about 'junior hires' is also very valid, with C++ you could /easily/ have a new guy write completely bonkers code, add subtle bugs that takes days to find, much much later. I had my own horror stories about that, and that's another factor that pushed me over the edge to drop the language.


> the new C++ compiler is throwing you a 10 lines error message just because that codebase is 10yo and isn't up to scratch with the 'new' way of doing things

Does this happen? I thought the committee was so into backwards-compatibility that C++ will also work on everything forever.


Sure. I remember one. For about a million years, you could declare a naked virtual method with int blah() = NULL; -- perhaps it wasn't /suposed/ to be used like that according to the standard... but it worked, and was used a lot, as it made perfect sense.

Then one day, you recompile it and no, it /needs/ to be ZERO and not NULL sorry.

But it's just one simple to fix example, in plenty of cases, especially as templates (and especially template instantiations) evolved, the whole thing would come crashing on you. For a while trying to compile templated code on MS, CodeWarrior and GCC was pretty much impossible without deploying ruses that made C preprocessor macros tame in comparison.


As you yourself admitted, it was not the standard way of doing things. Ever since the first ISO C++ standard, the syntax was =0. And it was never guaranteed that NULL (which is a macro) expands to just plain 0. So even back when it "just worked" for you, chances were good that it only worked on that one implementation that you had, and would've broken if you tried to use a different compiler.

Judging by your mention of CodeWarrior, this all sounds like war stories from pre-standardization days (and of course it still took a while after ISO C++98 was published, for implementations to actually adopt it).


> And it was never guaranteed that NULL (which is a macro) expands to just plain 0.

True, but is was guaranteed that NULL expands to “an implementation-defined C++ null pointer constant” (C++98 §18.1), where a ‘null pointer constant’ is “an integral constant expression (5.19) rvalue of integer type that evaluates to zero" (C++98 §4.10). So while it didn't have to be just plain (literal) 0, it did have to be a compile-time constant integer 0, which is otherwise just as good unless you're doing macro magic.

(This differed from C (ANSI era), in which a null pointer constant can alternatively have type (void*), and often does.)

The fact that a pure declaration requires a literal single character ‘0’ (rather than 0) is just one sad example of C++'s ad-hoc irregular overloading of things to mean different things.


It may be ad-hoc (although I would argue that "0" in this case is really just a token that's a part of syntax - basically a numerical keyword - so expecting a constant expression is too much). But, in any case, the main point is that it never changed - it was always wrong to use =NULL to denote abstract virtual methods, and there were always implementations that broke that. So it's a strange example of the lack of stability in C++.


Oh, ouch. I never knew about that one. I assume it's an effect of various "#define NULL 0" type things that got stomped by the introduction of nullptr in C++11?


All of my old C++ code broke when namespaces were standardized. Most of it could be fixed with "using namespace std;" at the top, but it wouldn't compile out of the box.


I doubt it and would love if the OP gave an example. One of the reasons that C++ suffers from bloat is they always strive to keep backward compatibility.


I was really looking forward to C++ ranges. However, that blog post by Eric Niebler felt like C++ ranges are trying to get everything right from a cs point of view, but in the process completely ignores usability / legibility, just like the previously introduced list operators that suffer from the requirement to specify start and end iterators. Some of the code samples are an atrocity. That C# example looks so much better to read and easier to understand.


I put a lot of value into the maintainability of what I'm writing and I think legible syntax is a language quality that's often minimized but provides a lot of value in terms of labour savings over the lifetime of a project. I've been working in PHP a bunch lately and the expressiveness there is amazingly strong, every time I go back to writing C++ it feels much more cargo-culty.

(Also, PHP isn't perfect, but it's really good at allowing clear readable code, while also allowing terrible code, also the performance is terrible... Basically, don't waste your breath on why PHP is terrible - it isn't, it's a decent language - you're just using it wrong)


Also PHP7 addresses a lot of the performance issues, getting it up into the node.js range and ahead of Python 3 / Ruby in the microbenchmark wars at least:

https://www.aerospike.com/blog/php7_php5/ https://benchmarksgame-team.pages.debian.net/benchmarksgame/...


If you think PHP is slow then you haven't used Python.


I concur. My biggest gripe with C++ today is that it's conflating two concepts: imperative programming (IP) and functional programming (FP) via templates.

Imperative programming has no future IMHO. Rust is going to the ends of the earth to make it "safe" but my gut feeling is that it will never be fully statically analyzable. Today I only see needless complexity that distracts from the underlying logic. I lost interest in this style of programming around a decade ago.

Functional programming via templates is an admirable endeavor, and I went all the way to the ends of the earth to make something of it. I was left with code that wasn't reusable (wasn't salable) so in the end, just wasted years of my life on needless complexity.

And both of the above only make sense from a performance perspective. An argument that becomes weaker with each passing year as processing speed improves.

I think the future will be something more like reactive functional programming, written in a high level language like Javascript and only dropping down to bare metal optimizations with something like Rust when necessary. It will work like the UNIX shell (which is the only proven model that's endured). So something like the Actor model where a series of black boxes are wired together with pipes and as much FP as possible, and any IP sections are treated like monads and flagged as having unknown side effects. ClojureScript does something like this, running its FP code to completion and then yielding until more input/output happens on the Javascript side. Another way of thinking about this is a spreadsheet with some imperative code within the cells where needed.

PHP is my favorite language currently because it has some notion of constness and fairly decent higher order functions (but who are we kidding, map and reduce give us everything else). PHP is a conceptually simple language that has some longstanding flaws with naming conventions that make it not as easy as it could be. It's largely succeeded in moving away from object-oriented programming, favoring a fairly simple interface implementation instead. I've never run into the implicit context problems that plague languages like Ruby (where convention over derivation is written into its DNA).

And PHP is fairly fast, on the order of 200 times slower than C++ but probably within the same order of magnitude as the newer C++ methods mentioned in the article. My gut feeling is that proper string handling, with every contingency accounted for, runs approximately the same speed in any language. I'm not willing to spend time and effort trading safety for performance anymore.


> Another way of thinking about this is a spreadsheet with some imperative code within the cells where needed.

Uhm, how are cell functions in spreadsheets an example of imperative programming?


Oh I was thinking of Visual Basic Excel macros, since VB is imperative.

I'd like to see a spreadsheet that uses something like Elixer or Clojure though because then any cells that don't have monads could be fully statically analyzed and form a purely declarative solution.

In other words, the cells could be decomposed further into cells containing individual functions instead of composed statements. That way the spreadsheet could be transpiled into lisp or a syntax tree. I'm using all these terms loosely, but they are equivalent.


Yeah, I really appreciate Eric's ability to explain the feature. It really needs a tutorial like this, not just standard docs.

That said, I think it's too complex for a portion of our team to grasp. So now what - do we use it, and alienate them? Or stay in the "dark ages"?


> Cognitive load is important

This, to me, is a very powerful statement that points to the major change between old C++ and the current model (which is IMO is a monster). It may have gained a lot in the process, but at a huge cost to conciseness and clarify of written code. And this may be fatal for a general purpose language.

Maybe as other languages with focus on cleanliness and clarity (Pythons, Kotlins and Haskells of this world) become fast and powerful enough for large projects the use case for simple and clear C++ code decreases. Then C++ can still provide benefit for, say, intermediate representations. But this complexity increase may be one of the last nails into its coffin as a general purpose language. My 2c.


> clarity [...] Haskell

no. Just no. Haskell is a lot like C++.


For the case in point, Haskell is much clearer. You can do Pythagorean Triples in one line. Google it if you don't believe me.

However, in general, (in my opinion of course) the cognitive load for Haskell is low as a novice, but gets higher and higher as you go down the rabbit hole toward learning, understanding and applying all of its power - which is constantly evolving not unlike C++.


Just because you can do it in one line doesn't mean that it has low cognitive load. In fact it probably increases the load since you have to think about all of the abstractions that are built into the language instead of reading them off of the screen. The three for loops version lays everything out right in front of your eyes.


Perhaps you're right. On the other hand, list comprehensions are a core feature of Haskell, which does not have something akin to a "for loop," so it's something a novice would be familiar with as part of learning the core of the language, just as a C++ novice would learn about "for loops."

So, I would argue the cognitive load is similar, and both lay everything out right in front of your eyes. Although, Haskell will do it with less syntax (alternatively, read that as "with more syntactic sugar").


Sorry, I was not trying to start a flame war. My point that there are now a bunch of languages (insert your favorite here) that used to be "OK for prototyping but not much more" because they were slow, promising but buggy or lacked some key features that now matured to offer a solid production choice.


Haskell 98 is a hell of a lot cleaner than C++98!


I work using C++17 for high performance applications, and I can relate to a lot of these gripes. I think it's a fair point that C++ is unreasonably complex as a language, and it's been a serious problem in the community for a long time.

One part that really struck me as odd is the focus on non-optimized performance. To me, this is an important consideration, but not nearly as important as optimized performance. Using techniques like ranges can definitely slow down debug performance, but much of the time it _dramatically increases_ optimized performance vs. naive techniques.

How do ranges speed up optimized builds? One of the best techniques for very high performance code is separation of specifying the algorithm and scheduling the computation. What I mean by this is techniques like [eigen](http://eigen.tuxfamily.org/index.php?title=Main_Page) and [halide](http://halide-lang.org) where you can control _what_ gets done and _how_ it gets done separately. Being able to modify execution orders like this is critical for ensuring that you're using your single-core parallelism and cache space in an efficient way. This sort of control is exactly what you get out of range view transformers.


> I work using C++17 for high performance applications

> One part that really struck me as odd is the focus on non-optimized performance

I'm guessing your high performance applications aren't interactive? When your application has to respond to user input in real time, a binary that is 100x slower than real time is completely useless. You can't play a game at 0.3 frames per second.

I would be interested in seeing an example of how Halide-like techniques can be used with C++ ranges. I am skeptical that you could get the kind of performance improvements that Halide can achieve. And of course you won't get the GPU or DSP support that is really useful for that kind of computation.


This is what RelWithDebInfo builds are for.

Don't make my Debug build into a RelWithDebInfo build or it makes it a huge pain to track down subtle bugs/errors in non-performance-critical unit tests.


This is dealt with in the article - debugging optimised code is a pain, even when you know what you're doing. The source-level debugging often doesn't work, variable watches often don't work (and this even though DWARF has a whole pile of features specifically so that this stuff can work...), and debugging at the assembly language level is a chore.


gcc has -Og (optimize without harming debugging) which is supposed to avoid these problems


Well, you aren’t rebuilding your binary every frame, are you? I might be missing something.

Also, I think build time is super important in most contexts - what I think is less important is runtime speed when you’ve disabled all optimizations.


It’s not the speed of compilation. It’s the speed the program runs with debug build. So runtime speed.

And for games you need decent runtime speed. If you cannot run your game in debug build one has to do good old printf debugging. And yes, if you cannot actually play the game (as in over 10fps) that means you cannot run it in debug build.


Are you confusing build time with performance of the resulting binary? I'm talking about the latter. Both are important and both are lacking with modern C++ in debug mode.

Edit: I see, I carelessly used the word "build" to mean a compiled binary, which was ambiguous. I've changed it.


Thanks for the clarification. I guess like all trade-offs, it's context dependent. I see the advantages of having a realtime usable non-optimized build for debugging. Since I use modern libraries like Eigen, that option has not been available to me for some time.

With "modern" techniques, the performance ceiling is a bit higher - whether that benefit is worth it depends on a lot of factors.


If you're doing Linear Algebra, you're kind of in a C++ sweet-spot, I think.

In particular, you can always debug a tiny version of whatever problem you're trying to solve, so you don't really care that much about non-optimized performance, and a lot of times you're willing to eat a long compile time if it means you squeeze out that last couple percent. Conversely, you care a lot about cache micro-optimizations and talking to GPUs and stuff like that, and generally you want to be just banging on some piece of memory you got from the OS, all things that non-C++ languages make extraordinarily painful.

Even Fortran, which the haters were trying to push as "just better" than C++ for linear algebra has really disappointed me.


> Using techniques like ranges [...] _dramatically increases_ optimized performance vs. naive techniques.

This claim will require some evidence. In my experience, it's extremely common for novice engineers to trade orders of magnitude in build time overhead chasing negligible runtime performance improvements.


I have a large C++ code base (mostly C++11), and I agree with this article. My code base is hilariously slow to compile, and the biggest single culprit is the variant header. I sometimes wonder whether I should just remove all the users to save time.

To add insult to injury, std::variant is a rather bad approximation of a sum type. variant<int,int> is only partially functional, and you can’t disambiguate the two ints by name the way you could in, say, Rust.

I think that a lot of these problems stem from two systematic problems

1. The desire to avoid modifying the language. A good modern language should have sum types. Heck, the ranges paper explains how ranges fundamentally have the wrong semantics wrt const and that this can’t be fixed without changing the language. So fix the language, please!

2. The fact that even new libraries are specified to be header files. Surely there could be a variant module even if modules aren’t really for full deployment.


> My code base is hilariously slow to compile, and the biggest single culprit is the variant header.

yeah, <variant> is so slow that I wrote my own (domain-specific but could be repurposable) variant-like code generator. It keeps the same API but makes everything sooooooooooooo much faster to compile - especially the visitations with multiple arguments.

(ugly fugly, full of dirty hacks, but it only had to run once or twice : https://github.com/OSSIA/libossia/blob/master/tools/gen_vari...)


Unfortunately modules don't solve the compile time problem, as demonstrated in the article, which matches my own experience. I was excited about modules until I tried them out and realized they don't help much in practice.

I hate to sound like a broken record but I've been telling everyone I know to try https://zapcc.com. If you are on Linux then it is a real solution to long compile times, especially for incremental builds.


C++ compilation feels like what you would experience if you could only understand a sentence by looking up each of its words, and each word was defined in a separate book, and each book was in its own shelf in an enormous room full of shelves, and one of the shelves was in a library on the other side of town, and one was in Alexandria.


I'm working with C++17 right now and I personally can relate to the author. Reading the discussions around C++20 has been an eye-opening experience for me. C++ was already a difficult language but has become a behemoth of complexity. But it has to be said that once you know "Modern" C++ it's a very productive language.

Somehow, it feels good to use it and after a while you find yourself reveling in complexity. I'm almost ashamed to say really.


My personal favorite metaphor for the complexity of C++ (and other languages, like Scala) is a hamster wheel. You enjoy writing it, but from the outside it feels a bit ridiculous to see the amount of energy dedicated to problems of your own making.

R-value references, for example. This is entirely a problem that C++ has created for itself with its copy semantics. From inside the C++ ecosystem, the language feature makes sense, but from outside the C++ ecosystem, the idea that there are now two types of references and two types of copy semantics triggered based on the implicit interaction of reference types, and you sometimes need to do idiomatic things like declaring undefined private copy constructors to lock down unintended implicit behaviors relating to copy semantics for R-value references... yup, hamster wheel.

C++ isn't the only language where I get this feeling. I think Scala is pretty guilty of this too. It's not that I can't grok it if I wanted to (survived my share of proof based, pure theory math classes back in college), it's that I think it's entirely too much energy to be spending on mundane code.


> two types of references

include normal pointers and it's three.


add const-ness and you get *2. Then ponder what `const &&` even means.


The complexity in the language is gameified a bit. It's a terrible sport where everyone suffers.


Things developed by committees often tend to lack the market-driven features of privately-owned products. On the other hand, privately-owned products are just that, and suffer from the usual consequences of closed systems. Would be nice if there was an intersection of the two, somehow (C# sort of comes to mind, as it was privately developed but has open-source implementations).

The one I always think back to is DirectX vs OpenGL. DirectX has had historically better driver support, documentation, and ready-to-compile example code (OpenGL has caught up a lot admittedly over the years, due to greater proliferation of OpenGL-driven devices).


Java has achieved a lot of this.


I echo the sentiment of the author. Also, It seems to me that in all three articles, what comes through is not that ranges will be cool or useful, but that coroutines are awesome. With this, I couldn't agree more. I sometimes wonder if we didn't have discussions about or these tertiary[1] TSes, we'd have coroutines in C++17 already.

[1] subjective opinion


It's amazing how much of a stir the C++20 ranges have caused. Very entertaining to follow.


I haven't seen anyone mention https://zapcc.com yet. I've been testing it and it works as advertised, cutting compile times by a factor of 5 or more in real world projects with zero code changes. It's both easier to use and faster than C++ modules could ever be, even in theory. It could go a long way to addressing one of Aras's complaints.

In order to improve C++ compilers, we are going to need to throw away some old assumptions about how they should work. Zapcc challenges the notion that the compiler should exit after compiling each file and start over with the next one. Perhaps some of the other issues Aras points out could be addressed by rethinking some assumptions. For example, tighter integration between the compiler and debugger could potentially make it feasible to debug optimized builds.


> Zapcc challenges the notion that the compiler should exit after compiling each file and start over with the next one.

this idea has been floating for a long time :

ftp://gcc.gnu.org/pub/gcc/summit/2003/GCC%20Compile%20Server.pdf

and

https://gcc.gnu.org/wiki/IncrementalCompiler

sadly, it seems that modules has put these approaches on hold, even though I am pretty sure that modules won't solve the problem.


> the time it takes for clang to compile a full actual database engine (SQLite) in Debug build, with all 220 thousand lines of code, is 0.9 seconds

I immediately thought about some Java (Gradle) projects at work that take 10 minutes to build, and other transpiled javascript projects that take an hour or so. What am I doing with my life?


An hour? For JavaScript? Wtf?


Babel + a million lines of Javascript, and a few other build steps. Durying development, only the required modules are built, so it takes only a few minutes to get up and running, but a full build takes ~1 hour and produces ~100mb in bundles.


Good article. Compilation time is indeed an important issue, and the main reason why I use C instead of C++ as much as I can.


As a long time lover of functional programming, and have not used C++ professionally since the 90s, this whole article feels sort of damning about the future of the language from a functional perspective.

The calculation of Pythagorean Triples is literally one line in Haskell (a language I have used for almost three decades), as a simple list comprehension. It also can then be consumed or used in any fashion, and is calculated lazily.

While I am not particularly adept at other "functional" languages, I would imagine similar brevity in F#, OCaml and possibly even Python (which I believe has some sort of list comprehension syntax).


How about going back to and forking C++98 (or whatever version preceded the first syntacical horrors such as reinterpret_cast<>) and start over with more C-like additions.


Yeah but what about implementing the real compiler after the spec? While you're at it why not make it a different language?


printf code should not appear in timed scope.


Why not? It clearly doesn't add anything meaningful to the run time, and helps avoid the question of whether the loop got optimised out.


I timed his code on Windows and it certainly adds to the runtime. To prevent the loop from being optimized out I created a pre-sized vector to index the ith ouput and print it outside the timed scope.

I have worked in the game industry and have many bad experiences with printf/logging consuming so much performance it makes games unplayable.


`printf` implements a barrier which cannot be crossed by an optimizer.


For a simple example, surely the easiest/safest way to make sure the loop remains a loop, is to look at the assembler output?


Meh. Aras is comparing apples to apples, so I don't see how it matters.


I agree its a general nitpick but it actual furthers Aras point if he removed the printf and the difference in performance (c-style vs ranges) was to increase.


I wonder if the state that C++ finds itself now is a result of sub-optimal compromises between different interests in the standards committee. Year long arguments of what ought to be included or how they ought to be done may cause folks to lose sight of what's important, you see.

I still like C++ though. At least a subset of it anyway.


A pretty good summary of the state of C++. I actually enjoy the language and most of the new features. The 'modern' part is not at fault here. This has always been a problem. The cult of generic metaprogramming was always a pain to deal with. In part, the new language features can help reduce over reliance on templates. As a community we must push for features that can get is out of this situation, like a sane modules system.

I think Bjarne Stroustrup is partly to blame here. He has succeeded in creating a versatile and popular language but has given little priority to problems like build time, error messages and debug performance. These sound like petty, practical problems that the tooling guys should eventually figure out. Except they aren't. They are hugely dependent on the language design.

Another thing we are missing is a high profile figure that will show the way on correct patterns and that will literally publicly shame the authors of too "meta" template heavy libraries. You are not smart if you are writing these monstrosities. It should not feel good. Making it simple requires much more intelligence.


> The cult of generic metaprogramming was always a pain to deal with. In part, the new language features can help reduce over reliance on templates. As a community we must push for features that can get is out of this situation, like a sane modules system.

The "cult of generic metaprogramming" is the very community pushing for those features.

> literally publicly shame the authors of too "meta" template heavy libraries.

This is pretty harsh. I agree that metaprogramming abuse in application code is a problem, and it's not something that should be advocated. However, there is a big difference between promoting C++ literacy and saying "everyone should commit metaprogramming code at work". The blame for metaprogramming abuse lies _squarely_ on engineering teams with poor quality control, not on library developers who like to push the language to its limits in their free time.

Everyone knows that C++ is a language of footguns. Metaprogramming is one of them; Boost is a mixed bag. Every successful C++ engineering team enforces coding standards to address this.


> publicly shame the authors of too "meta" template heavy libraries

Wouldn't they say we should instead shame the people who can't read them? What makes you right and them wrong?


It's hard to change people, it's easier to change code.


I think that people have been taking Eric example too seriously. I can't speak for him, but I doubt that Eric was suggesting that such code would be routinely written with the range library, he was just showcasing as much as possible of the range library in as short an example as possible. In a way it was designed to maximize complexity.

These excesses have in fact a positive side: the extent that people will go to get a specific functionality (in this case lazy ranges) as a library is a signal that the feature should be part of the language.

This happened with lambdas, boost.lambda, boost.phoenix and to a lesser extent boost.bind showed that the functionality could be implemented as a library, people stated playing with it and demanded the functionality to be baked into the language.

A similar thing is happening now, there is a push to extend the proposed await syntax to become a more general (and more efficient) continuation syntax that, among other things should lead to built-in support for lazy ranges.


I can say that I have been guilty of writing such code, and people I worked with have too. When you are given a tool, you want to use it, especially if it gives you a lot of bell and whistle.


The desire to overcomplicate and overabstract things does seem to have taken hold in the C++ community within the past few years. It's not anything new, however; just that for C++ to get the "Modern" treatment, is relatively recent. (Look at the Java and C# communities for example. What they don't have in terms of complexity in syntax, they make up for with an abundance of classes.)


The problem is not modern c++, it's only when c++ tries to be a functional language that it becomes unreadable.

As I see it, there are multiple "sub languages" in c++:

-Macros: use as little as possible.

-Classic C: use only when interacting with C libraries, or in the few cases where it's more convenient (printf over cout).

-Classic C++ (classes, etc): use it all the time.

-Template meta programming: use only for simple things, eg containers.

-Functional C++: try to avoid.


I want to say one word to you. Just one word. [...] Are you listening?

Modules.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: