“Modern” C++ Lamentations (2018)
aras-p.info
aras-p.info
All programming languages have their intrinsic features, but they also have a culture that surrounds them.
C++ has a rather odd culture... it's very a very fundamentalist/extremist one, all providing the optimal way to do things with no / little extra runtime overhead, but with no regard at all to compile times, readability, etc. It's not necessarily a bad thing, and it has its uses for games, ultra low latency software, all that stuff.
I am proficient in C++, I've worked in it for a bunch of years and can get stuff done in it and solve problems and ship software. C++ job interviews tend not to pay much regard to that, and focus on the arcane finer points of move semantics, rvalue references, ranges, concepts, all this new stuff... which has its place, but there is a lot of that to learn, and in many cases it just doesn't matter. After all, the language existed without it for a bunch of years.
Compare this with Java, or python or whatever, where the culture and focus is very much about "can you solve a problem / implement a solution using this language <or not>?" rather than "of the dozens of different ways to do this... what are the differences between each one".
For me, I really don't mind using and programming in C++, and I would probably even choose it for many / most projects, but I can do without the C++ people and culture.
I have been writing C++ professionally for 15 years and agree. It has excellent ecosystem in several domains. On the other hand, in some areas the user needs to deal with incomprehensible and user hostile tooling and ecosystem glitches.
The weird thing is that more often than not the latter pathologies are considered "just fine", which just baffles me. Modern setups like Rust and it's tooling has demonstrated clearly that high performance native language has no intrinsic requirements to be user hostile.
Some people like to think that dealing with C++'s unnecessary complexities is somehow virtuous, when in fact they are a a bug of the platform (but one you need to deal with since if you need to write C++, you really need to write C++).
We cannot optimize much in Java or Python, so of course we do not focus on the different ways of doing the same thing. C++ is needed in cases where the differences matter, so of course we care about the differences.
We can't push Java or Python to be maximally efficient (not wasting a single cycle or byte over the absolute optimum), so we don't use them for use cases where that would matter. Therefore, we don't try to push Java or Python optimisation as far as possible. Being faster is always nicer, but it can never be essential to be optimal, because you wouldn't be using those languages if it was.
Whereas we can push C++ that far, so we do use it for use cases where absolute optimality matters. Hence the community around it values the ability to do that, and we end up with all sorts of weird and awkward stuff.
C++ also has a lot of weird and awkward stuff that is nothing to do with this, i should add. Most of that is ultimately due its age and C heritage, i think.
OK, so there are limits. Further though the fact that you sometimes want to be able to do something did not require that C++ insist upon you doing that all the time.
There are places where they didn't. HN recently discussed structure layout and padding. You can write a C++ program with explicit structure layout and padding (I think this may not be portable though) which can significantly alter performance - but you don''t have to. If you're OK not worrying about it, C++ won't optimise layout for you (as Rust would) but it also won't ask you questions about alignment and layout, they're just left alone.
But for some things they insist. Manual memory management for example. The language doesn't (in practice) provide garbage collection, so you are responsible for picking up the trash even if you don't care about that problem. Arithmetic is another, even if it's not performance critical for you, C++ insists you need to cope with the vagaries of machine arithmetic anyway and care about things like overflow and Not-a-Number.
And this burden imposes a real cost on programmers which, at the end of the day, means they actually produce slower/ bigger code than they might have.
You're referring to the "zero-overhead" goal of new C++ features. I think the above poster was talking more generally about one's ability when using a language to implement some critical subset as fast as possible or with as little memory use as possible. I suppose it's arguable if you're still doing C++ when you use intrinsics. Though I think pragmas are certainly fair game. At least C++ (and C considered as a subset) is the biggest high-level language with such a possibility.
It think this is attributed to fundamental rule of not breaking legacy language features. That is the number one rule (much like how Linus is iron fisted about not breaking user space in the Linux environment). However, there is a lot of wiggle room in the way binaries are generated, so compiler authors can get really creative with their optimisation efforts.
No. The number one rule in C++ is "don't pay for what you don't use".
https://build-bench.com/b/FW3EPgB1t0fmpIr1TB_vJbbb0Vw
Which is exacerbated even more by the leaky #include system, where you can easily end up pulling enormous standard headers into translation units that don't even reference them directly. The only reprieve is to ban most of the standard library from your project and write your own leaner version from scratch, as most big C++ projects end up doing.
I'd start with the very basics, such as using forward declarations, encapsulation, and the pimpl idiom to not drag unnecessary #includes into your translation units.
Also, the compilation bottleneck sometimes lies in IO, thus moving your build folder to a RAM drive can speed up things significantly with zero effort.
It might be easier to just have quick compile times though!
Is this really all that unique to C++? Or are you saying that it's just particularly painful for C++ due to where it started from?
Also in java everything has to be under the constraints of an object before you can do the stuff you want to do. It does seem more extremist to me because it's literally encoded in the compiler ruleset.
This is C++'s moto and core feature.
Don't think it's for you? Then it's not for you.
This is new stuff to learn but once you do you realize it's super helpful.
inline constexpr auto for_each =
[]<Range R,
Iterator I = iterator_t<R>,
IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun)
requires Range<indirect_result_t<Fun, I>> {
return std::forward<R>(r)
| view::transform(std::move(fun))
| view::join;
};
Unless it solves world hunger or COVID permanently I am not typing this abomination, much less try to understand it. Very seriously whatever "team" is working on C++{20,21,..} should be disbanded and sent to someplace where they don't have access to computers. Nothing modern including Kubernetes ecosystem induces this much rage in me. template<Range R, class Fun>
auto for_each(R&& range, Fun fun) {
return std::forward<R>(r)
| view::transform(std::move(fun))
| view::join;
}
I.e. I wouldn't bother to type check Fun and I would use a function instead of a function object. Both features are important for library code though.Forward and move are still pointlessly noisy, I wish there was a keyword for those (especially forward). And I wish we had proper UFC instead of abusing pipes.
Err(Err::Error(Error::new("c1", ErrorKind::Digit))));
either, since I just assume its not the language's fault.Also I don't have as much of an issue with sane subset of C++ as a language - it's the modern crap they keep adding pompously without regard to complexity and usability that irks me.
This can terribly fail if you have header files depended on by everything that constantly change - but honestly... that's a pretty bad code smell so either make an archive that's a dependency of all those others (and occasionally feel real pain when the header file actually gets updated) or else refactor things for sanity's sake.
A year and a half into my first job I was tasked with revising the build system and there are some really effective things you can do there without very much training. You want to lean on `make` a lot - no seriously, a lot - but if you do you can make some amazing things happen. You've also got a lot of cross platform tooling if you need to compile on different architectures - though scripting how those interact does get progressively more difficult in those cases.
1. Just in case you're unfamiliar - linux static libraries are essentially just an archive of a bunch of object files... so you can freely use ar to manipulate them in all sorts of ways https://tldp.org/HOWTO/Program-Library-HOWTO/static-librarie...
If you have very generic and flexible templates defined in a static library the library needs to have actually forced the compilation for all the types you intend to use. So I do agree that templates remain a big pain!
* External libraries don't do this. See: All those "header-only" libraries you see, starting back with boost
* It's a total PITA once the types get complicated
Source: Have worked for 10 years on a large codebase with "template auto instantiation" off.
The fact that we are still dealing with weird problems with pointers, the fact that C++ has lambdas without garbage collection (explanation of the problem is too long for this comment), and the incoherent type system are much bigger issues than weird syntax or long compile times. The C++ feature set is a bunch of semi-compatible, sometimes outright incompatible (ahem destructors vs. exceptions and coroutines), ideas that keep getting extended further from compatibility by the standards committee.
Here's a poignant anecdote. At one point while working on HHVM at Facebook I finally snapped, spent 2-3 days doing nothing but optimizing the build system to speed up trivial incremental compilation (e.g. a whitespace change). My efforts resulted in... 24 seconds best case. I spent years on that project pipelining my coding, that is, making a small change, asynchronously launching a build, fixing issues from the previous build attempt, ad nauseam, and the pipelining was oftentimes 3+ deep due to latency. That cognitive load severely impacted productivity.
That said, I agree that C++ has a lot of other terrible problems too!
We once reimplemented automake in KDE land (into a tool 'unsermake' by Stephan Kulow), for a few reasons but foremost among them was that it reduced compile times, sometimes drastically.
To get to something that fast for incremental builds of course requires some tweaks from the buildsystem defaults:
- clang instead of gcc (I use clang on mac, windows, linux)
- ninja instead of make
- -gsplit-dwarf for debug info
- mold instead of ld (I used lld before which was already nice but mold is bewildering)
- PCH (very easy with cmake thankfully !)
- split in shared libraries of adequate granularity, e.g. the software is split in ~40 plug-ins (although with mold I'm not sure this is even relevant anymore, the complete link step if I don't use shared libraries is not that slow).
I've encoded most of these in my cmake toolchain-generator, cninja: https://github.com/jcelerier/cninja/
To give some reference, my hardware is a 8c/16t intel 6900k.
Edit: I did a complete build with everything statically linked instead of through shared libraries. To give a reference: lld (which is already fast compared to GNU ld and gold) links the entire software in 0.47 seconds ; mold links it in 0.3
> split in shared libraries of adequate granularity, e.g. the software is split in ~40 plug-ins
So you had to alter your project structure to improve build times?
I don't believe in accepting things just because some engineer choose a default under some deadline 25 years ago
> So you had to alter your project structure to improve build times?
no, the software was designed as-is from the very beginning (and it's an architecture I'd recommend for any software which is supposed to be extensible from its very inception, it worked out very well)
That's not the point. The point is the "standard" is suboptimal enough that you basically have to change it. Don't think that applies to much of the competition (with the exception of Java that isn't really competition?).
but there's no standard, just CMake defaults that I change ? It's not more standard to call /usr/bin/clang++ than /usr/bin/g++ (and if I was running freebsd instead of linux, as far as I know that's what would happen by default) ; likewise, other build systems like Meson use ninja by default (and that does not make ninja any more of a standard than make is when using cmake under GNU/Linux). Those are just tools in a toolbox.
Except it's 3 seconds per file.
The project I'm currently working on has 1.5 million lines of code spanning ~5,000 c++ source files.
At 3 seconds per file it would take over 4 hours to do a clean compile.
Luckily for this project it's not 3 seconds per file, and a full rebuild only takes about 2 hours - which is still a major pain.
The concerns raised by the OP are completely valid, and cause issues for any medium-large c++ project.
I'd love to have faster C++ compile times.
Not even half as big as yours, but for what it's worth my incremental build time is a bit under 3 seconds (and is almost entirely taken up by linking).
A full rebuild for me (on a Ryzen 5950x, doing the build in parallel across all 32 logical cores) takes about 80 seconds.
I feel that for the creative projects I work on, having an iteration time under five seconds is the most important thing for me. Once I've written the code, if that code isn't compiled, linked, launched, and visibly running on screen within five seconds of when I finished typing the code, then I'm going to be tempted to check email or otherwise context switch while I wait for it, and it might as well have taken half an hour.
I spend a lot of time early in development trying to figure out optimal approaches to getting that iteration time down, optimising for incremental build times, link times, and debug build launch times.
I find it pays huge dividends, and quickly, for me.
It's not an inherent property of the language. With some care, it is possible to write C++ code with compilation speed comparable to good old C, even in large projects.
The worst offender is usually templates. Especially third-party libraries which use them heavily, like boost. Ideally, don't use these dependencies. Second best option, only include these libraries in *.cpp files which actually use them, and keep that number to minimum. When absolutely necessary, note C++ allows to split templates across h/cpp files; just because the standard library is header only doesn't mean non-standard templates need to follow the convention.
Another typical reason is insufficient modularity of the code. It can cause some of the source files (especially higher level ones, like the one containing the program's main function) to include ~all headers in the projects. The fix is better API design between different components of the software. A good pattern for complicated data structures is pure abstract interfaces, this way the implementation stays private, the consuming code only needs to include the (presumably tiny) interface definition. Another good pattern is FP-style. Regardless on the style, I sometimes write components with thousands of lines of code split across dozens of source/header files, with the complete API of that component being a header with 1-2 pages of code and no dependencies.
And of course you want all the help from the toolset you can get: precompiled headers, incremental builds, incremental linker, parallel compilation, etc. Most of these are disabled by default, but can be enabled in the build system and/or IDE.
Luckily there is buyin not just from developers but also senior management to fix things, but it’s also in an extremely risk averse industry so changes need to be slow and careful.
The fact that there is an "only" in that sentence, tells me everything that is wrong about C++ compile times, and makes me happy I work with Golang.
And even if a project was that size, it would compile faster than the C++ equivalent.
Splitting the project into libraries that don't need to be rebuilt for normal development changes eliminates 90-99% of your build time.
We can get into separate dwarf files after you get the basics down.
..for your particular use case*
* your particular use case may not be representative
If you think lambdas with GC are a good idea then you would not use C++. You would maybe use Go.
> Compile times matter in extreme cases, but 3 seconds is not really something I would worry about.
I think a lot of people would be very happy if their projects compiled in mere 3 seconds.
Memory layout, yes.
CPU instructions emitted? Not so much.
Technically you could have different code paths and inline asm, but that's not really specified by the C++ standard either.
Lambdas with GC are an extremely good idea, because of the silently shared environments and what not. Whether C++ should have lambdas if it can't guarantee GC is a completely different thing. C++ doesn't have to include everything to be fashionable, after all.
In 2022, this applies just the same to someone who wants to use Zig or Odin.
If you want to be really high level and not worry about memory, you would not be using these languages to begin with.
The issue that I think the top comment is alluding to is that lambdas cannot capture by reference and then return, due to that stack frame becoming invalidated, which is a fairly large limitation that prevents swaths of lambda heavy code from working.
I personally think that this is a fine tradeoff for what C++ is: there's still plenty of utility in lambdas for simple higher order functions like maps and filters.
The large majority of lambdas that I have written in c++ didn't need to do any dynamic allocation because they were used in (and only in) the scope where they were defined. Most of the time the compiler probably just inlined them.
Managing the lifetime of referenced objects is something you always have to worry about in c++ anyway, lambdas don't change that.
You can also just capture by value, rather than by reference. If you really want, there's also reference counting with shared_ptr.
Those things are misnomers anyway. Having access to an enclosing environment via a free variable in a lambda expression is completely orthogonal to values vs. references. I suspect they should have named the whole thing differently.
Except with Lisp, you aren't starting the REPL potentially every couple minutes. I've worked in many Lisps and it's not uncommon to keep a REPL open for days (recent project had one open for _weeks_)
Back when I worked in C++, compile times drove me crazy. 3 seconds wasn't remotely normal even for tiny projects because there's the overhead of actually starting the compiling process, reading output, thinking about what you saw, repeat.
Compile times in "the minutes" was much more normal.
Fortunately these days I work on more saner projects which use distributed builds and a properly parallelized makefile and a full build is just a 2-3 minutes.
Incremental builds are still slower than ideal though.
And that was for C, which is relatively straightforward compared to c++.
I suspect it would take months to sort out the project I am on now, and even then full builds would still be slow.
To suggest it could be fixed in hours is ignorance.
0: https://lore.kernel.org/lkml/YdIfz+LMewetSaEB@gmail.com/T/#u
On a tangential note, the amount of Greenspunning C++ has done over the last two decades is truly impressive. There sure is a lot of syntactic noise though.
C++ is predominantly a language that's oriented around value types instead of reference types. If you have a reference it's because you've taken extra steps to ensure the thing you have is a reference. As opposed to Java, Python, C#, Javascript, PHP etc where most everything is a pointer to somewhere.
Suggesting that C++ adopt GC to solve the problem of capturing references on the stack in lambdas is akin to suggesting that the Netherlands solve its biking/transportation problems by subsidizing cars and gasoline. It's not even wrong.
I have no idea what happens in this situation, but it is not at all impossible to wind up with something like this in a complicated and large codebase.
So while C++ lambdas are useful as anonymous functions and when passed "downward" (if you are very careful about when and how ownership is transferred), the full power of lambda expressions is not really available to a C++ programmer. It is unfortunate that lambdas are so crippled in C++, because lambdas could become the basis for even more powerful language features (pattern matching, continuations, etc.). Of course, the C++ standards committee continues to see lambdas as a kind of shorthand for defining "function objects," rather than as a first-class type that can be used to define more advanced features.
RAII is not all it's cracked up to be. It works for memory management as long as you are careful, but it is a lot less robust for other resources. For example, fstream, a textbook example of RAII, awkwardly forces programmers to explicitly ask for exceptions to be thrown. This is necessary because close can potentially throw an exception, but RAII means that you are calling close in the destructor -- and exceptions cannot safely propagate from destructors because destructors are called as the stack is unwound during exception propagation. This is one example of what I meant when I said C++ features are not compatible with each other.
Ironically, adding a garbage collector could fix RAII by un-crippling lambdas. The idea here is to create something like the conditions/restarts system from Common Lisp (or call-with-current-continuation from Scheme), using lambdas to support CPS conversion and using continuations to implement "exceptions" (i.e. "conditions"). Garbage collection would allow more liberal use of lambdas, and thus the compiler could automatically generate lambdas to implement other language features (just like it generates an array of function pointers to support polymorphism for classes). CPS conversion involves (among other things) taking code appearing below a function call, wrapping in a lambda that captures the stack frame, and passing that lambda as an (implicit) argument to the function (just like "this" is an implicit argument to member functions). "catch" blocks are similarly wrapped in lambdas ("continuations") and passed as arguments, and "throw" statements would call the appropriate exception handling argument ("return" statements call the non-exceptional continuation). Since everything is a tail call, you wind up heap-allocating the "stack frames" and relying on the garbage collector to deallocate everything (at this point it should be clear that explicitly managing ownership in this setting is totally unworkable). Now instead of unwinding the stack you have control flow jumping directly to your exception handlers, which can potentially return control flow to a defined "restart" to avoid unwinding the stack or else unwind the stack at the end of the catch block (unless another exception is thrown, in which case unwinding will be further delayed). Destructor exceptions are no longer a problem, because they can only be thrown when there are no active exceptions (or more precisely, when there would be no ambiguity about which exception is "active").
Sure, C++ wouldn't be C++ if it did what I described above -- unless you had the ability to add a declaration that you want it to happen for certain functions/class methods, for example by having a "collected namespace foo {..." syntax or whatever. It would be a headache for compiler writers to have to manage two separate function call paradigms, but it is not technically impossible to mix CPS code with C-style call stack semantics, and anyway CPS conversion is not the only way to implement what I described. Sadly the C++ standards committee does not see the poor compatibility between features as a problem that needs to be solved, so I doubt anything like this will ever happen.
On the other hand lambdas would be significantly less useful if any use implied an allocation and GC was forced.
The issue with fstream is that if you really care about the error you call close explicitly, otherwise you let the destructor swallow it.
In fact if you really care about errors and you would use a transactional interface, with an explicit commit and a no-fail implicit rollback in the destructor. RAII works perfectly for that.
Regarding CPS, that's exactly what the new coroutines do. They end up heap allocating the stack frame, which has proven extremely contentious to say the least. Still, RAII works just fine there.
In an IDE, your program is partially compiled with every keystroke. So compilation time is a paramount concern, not in extreme cases but always, everyday, all the time.
> 3 seconds is not really something I would worry about.
That's 3 seconds for a single translation unit, and a short one at that with a single non-templated function. Now compile 1,000 translation units, each of which being, say, 3 times as complex (so, a decent-size project) - and your compilation time has become 144 minutes, nearly 2.5 hours - instead of 192 seconds, a little over 3 minutes.
Now, it's true that you don't recompile your whole project every time, but still, the difference is huge.
> The fact that we are still dealing with weird problems with pointers
Actually, this has been turning into a non-problem in C++ with smart pointers and spans. More generally
> C++ has lambdas without garbage collection
I wrote this: https://stackoverflow.com/a/48046118/1593077
a few years ago. You're welcome to link to an explanation of why you believe GC is necessary when using lambdas.
> bunch of semi-compatible
To some extent, certainly. But you need to account for two points:
1. C++ is multi-paradigmatic. You should not expect to use all features together.
2. While this may seem weird, or ridiculous, C++ is a work-in-progress language. There are issues which have been known for decades and are only now being addressed, or not even now. I mean, we've needed (some of) the ranges functionality since the STL was introduced in the early 1990s, and it has just now made it into the language. This may not be a good thing but it is _a_ thing, so it's actually not the case that the language
> keep getting extended further from compatibility by the standards committee.
Compile times are definitely a headache for me.
a) #include <vector> -- Lots of dependencies, but I know it works and I don't want to reinvent the wheel
b) #include <my_vector.h> -- Ok, I'm going to reinvent the wheel, but at least my wheel might have slightly fewer dependencies.
c) //#include <vector> // I'll just do without the wheel entirely
3 seconds for 50 lines of code. How many seconds for 100k lines of code?
Benchmark it, otherwise it's sadly not useful
3 seconds to compile less than 100 lines of developer-written code?
So a lot of times if you want the captures to stay alive but don't want a deep copy, you'll make a std::shared_ptr<> and capture that, which leads to reference counted captures.
With GC, this might cause a small unexpected leak of some kind. But in a language like C++ it can be Undefined Behaviour and all bets are off.
In C, I think it's reasonable to assume that there is a linear relationship between the number of expressions in ones code, and the amount of assembly instructions it generates.
No such relationship exists in C++ code.
The problem is that due to this zero-cost mentality, brain-dead simple libraries, such as iterators, often have dozens of layers of abstractions, which all show up in a Debug build.
This makes Debug builds harder to well, debug, as you have to understand all these abstractions, (and the reason why the designers thought they were a good idea, but that falls under the umbrella of psychiatry), as well as making the build unusably slow, of†en forcing C++ devs to debug Release builds, which means that they are staring at 3 lines of x86 assembly, with hundreds of lines of 'helpful' compiler generated source code around it.
How did that dagger end up there?
Stuff like `iota` being obscure is less convincing. It wasn't invented here and it is anyway something you learn once and then it's part of your vocabulary.
As a side note, a major folk selling point for modules was that it might help with compile times, and a ton of work has gone into it in order to try to deliver. I haven't checked in on it in about a year, but my understanding is that it has largely failed on this front.
Saying that there is a general trend/problem in C++ projects with very long compile times for even simple examples is, I think, an extremely well-grounded assertion (although ranges-v3 seems to be the new champion there).
But really, there's nothing new here. When a developer brings in a library to their project there are always a bunch of dimensions to evaluate. Compile time is simply one that C++ developers have to evaluate, just like developers who rely on NPM have to ask themselves: what happens if $author goes nuts?
FWIW, I think it's too early to declare failure on modules. The standard library hasn't been "modularized" yet, but the three major compilers all offer ways to import, rather than #include the standard lib. I just did an experiment the other day and switched my current hobby project to use import for my standard library dependencies and it nearly cut the build time in half. Very promising.
Some people say boost isn't "modern", because it has accumulated a lot of historical cruft to support buggy compilers (old versions and varied vendors). I don't think that's the most accurate interpretation. I think "Modern C++" became such a big thing after C++11 because the effort required to make and use boost style libraries dropped significantly -- no longer do you need your project to either depend on boost, or implement its own version of some NONCOPYABLE macro to disable copy/assignment(/move) operators, or what have you. I think saying "modern" means removing the hassle along the way to implementing boost, is to miss that the point is to be able implement things like boost -- implementing/using things like boost is what's "modern". The fact that an absolute ton of the new standard library features have an origin story in boost only further confirms this.
One thing I forgot to mention when I distracted myself with modules, is that I totally agree with the author that the problem is also about debug build runtime performance, not just compile time.
Interesting to hear about modules. I'm out of the C++ game but I'd be happy for it to be working well.
(I have no horse in this race) But if a big part of their complaint is compile times, Rust may not be the best example of a contender.
Sure, it's kinda slow when compared to something like Go, but the amount of extra work rustc does compared to pretty much every other compiler is very big. Taking this into account, I can forgive it.
But even with all that said, Rust has a compiler performance team that is constantly trying to improve things and keep track of regressions. I dunno if C++ implementations (eh gcc) have something similar?
But yeah, at the end of the day, I think compilation times are important, but not as important as some other aspect of the language... Like, in C++ you can't even catch some errors until you start compiling and it fails in template expansion, while I usually don't have to compile Rust until I'm checking business logic. This allows me to focus on the problem at hand, not the language.
Sure, Rust compiles slower, but in C++ I have to wait on a slow compile just to find something dumb like a typo. And it doesn't even have the borrow checker. I have to make an exe that doesn't work to see if I made one typo. And then the errors are so bad.
https://clang.llvm.org/docs/ClangCheck.html
you will need a Compilation Database (CDB) in a compile_commands.json file. I know CMake knows how to generate one, but the fact that other build systems don't shows how fragmented and user- unfriendly the cpp ecosystem is.
Maybe I'm setting the bar too high for an old technology? I don't know.
What IDE (or just DE) would you recommend that works well?
I'm not on my work machine now. I don't write C++ as much at my company anymore, as I was pushed into a more (regrettably), so I can't recall an example from the top of my head now. But there were definitely instances where you can pass in something, and it doesn't error until you hit the "build" button (or run a build via cli). And even then, the editor doesn't report where the error is in the "editor area", you have to read the file/line in the console.
We have a root project that's fully CMake, that orchestrates our sub libraries (also using CMake, declared with ExternalProject_Add) so the dependees are built before their dependents (and also to handle final packaging). The subprojects are added as git submodules.
I tried VSCode and QtCreator but none of them seem to support CMake multi-projects defined in this way. I have to create a build directory per subproject, which can be painful when we have so many subprojects.
well, those are not sub-libraries but external projects with their own build-system. I have sublibraries in submodules and add them with add_subdirectory and everything shows and is auto-completed correctly.
imo this is the big takeaway here
https://en.wikipedia.org/wiki/APL_(programming_language)#Exa...
At least the Hello World example is readable...
edit: this is actually a different demo, but there's a similarly impressive one focused on showing APL, from the 70s I think? Will update if I find it!
“Modern” C++ Lamentations - https://news.ycombinator.com/item?id=18777735 - Dec 2018 (249 comments)
C++ made it so ugly. Especially if you wanna implement a custom range.
I can recommend this article by Andrei Alexandrescu, if you are interested in the idea of ranges.
https://clang.llvm.org/docs/AttributeReference.html#overload...
Write new code the new way, and leave old code that works alone. There will never be a case where you wonder why old code looks like old code. Continuing to write new C++03 is just masochism.
You might find reasons to switch some old stuff to use move semantics, eliminating reference counts, which is a small job. Leaving alone stuff that doesn't need to change needs only discipline, not time. Discipline builds with use. I recommend it.
disagree. this keeps things simple, readable and maintainable. beyond STL i havent found need for any new data structures or algorithms. i roll my own for domain specific (audio/graphics).
> Discipline builds with use. I recommend it
sure daddy. perhaps if the c++ committee had discipline they wouldnt have added these crazy things to the language too and made it a mess that it is today. however we masochists are pleased with the concession offered by stroustrup's one-liner: 'if u dont use it, u dont pay for it'
Unique pointers pre C++11 are fundamentally broken, and smart pointers are the poster child for modern C++. Other features like nullptr, enum class, range based loops, and constexpr provide huge gains in readability too.
> i havent found need for any new data structures or algorithms
That's great - however the unordered contains are for many people a drop in replacement with a performance boost that came in C++11 .
Along those lines, I think the real disconnect in the argument here is in terms of what kind of problem people are programming for. For many applications, advanced C++ is solving problems they don't have. The above poster mentioned audio. The data there is in single (or perhaps few) big buffers with a very simple and regular structure determined by international standards. They only need a few simple pointers. Or maybe just std::vector. Similarly just a few types also with real-world meanings. Debugging may require them to check the processing in ways that abstraction, type-checking, etc. will only get in the way of. And new features may not work within whatever abstractions had been put into place for previous features anyway, because the features relate to the underlying data.
for (std::unordered_map<std::string, int>::const_iterator it = some_map.cbegin();it != some_map.end(); ++it)
is pretty damn unreadable (and for bonus points it doesn't compile) compared to for (const auto& keypair : some_map)
Which doesn't have the possibility of the issue in the previous snippet (cbegin != end rather than cbegin != cend)> For many applications, advanced C++ is solving problems they don't have.
No-one here is talking about advanced c++, we're talking modern c++.
> The above poster mentioned audio. ... They only need a few simple pointers...
And we all know that audio is immune from massive security vulnerabilities, right? [0]
"New" features like nullptr (instead of NULL), unique_ptr, move semantics as examples can just flat out avoid classes of bugs that come up in low level programming, and features like constexpr and static assert can make runtime checks compile time checks instead. All these features can be escape hatched if you really really need to just cast to a void pointer to fill a buffer at the end, but the surface area for nasty bugs is significantly reduced if you do so.
[0] https://msrc.microsoft.com/update-guide/vulnerability/CVE-20...
Are you saying the creation of security vulnerabilities has decreased in the last ten years.
edit: and as for: "No-one here is talking about advanced c++, we're talking modern c++."
I am. Reconsider your examples in ternms of not just whether someone needs the improvement, but also whether someone actually needs the preceding alternative you are complaining about.
I deliberately made it not work - calling cend instead of end fixes the issue (one which isn't possible possible the range based loops)
> Your second one hides almost everything from me. If the second one fails to compile and gives me a paragraph-long encrypted complaint, I need to somehow half-comment the thing out and create a bunch of test code so I can go in the debugger and figure out what you are actually doing.
And I disagree here. In practice the only issue I've ever seen with range based for loops at compile time is no iterator support. The error on clang and msvc is incredibly clear here. Chances are the compiler error message about comparing a const iterator to a non const iterator is going to be more obtuse
> also whether someone actually needs the preceding alternative you are complaining about.
I cannot think of a _single_ situation where the NULL macro is required where nullptr wouldn't be an improvement. I'm not saying don't use the fundamental constructs when they're needed, I'm saying use the modern alternatives when they're suitable. Unique_ptr and move semantics alone eliminated practically every use after free bug I've seen in the last decade, for example. Enum class is another great example - legacy enums are absolutely chock full of foot guns and I have found countless bugs where someone just passes garbage through. Again those bugs just don't exist with the modern replacement.
Without these the complexity in your example disappears, and every word has direct intuitive meaning. Except for the ugly type expression on the left (which is kind of thing one must love if you endeavored to be a C++ programmer in the first place, though personally I do agree it's increasingly hard to read beyond the most basic cases). Nonetheless, using auto is the cause of the problem I was referring to. This eliminates the remaining half of the ugliness, but now if the code inside the loop chokes in your inputs, I have no idea why. Maybe if I'm lucky the compiler will give me a paragraph that I can try to parse descriing the mess that the object was compiled into, or maybe it will just say "Can't find a suitable template. Have a nice day".
If you need to iterate over an iterable xs and discover you need the index as well, in Python you shouldn't switch to a loop over the index, you should switch from:
for x in xs:
...
to: for idx, x in enumerate(xs):
...thank god for that. and also thank god that it is not a proprietary language/platform. eg., Apple otherwise we would be mandated to write in the latest C++.
>That you have failed to learn to understand those reasons is not a fault in the >Standard. Blaming your own failure to to learn on the committee does not fool >anybody
what you call as my failure to learn is not remotely unique to me. many others more eminent and eloquent than me (and you too) have said more unkind things about C++, least being the phrase 'cognitive load'.
the 'trouble' with software is it its impossible to leave it alone. an 'update or perish' sword hangs above it. and C++ has swallowed the bait totally. it has competed with the joneses all the time: oh Java has garbage collection, why cant C++ have that too?..oh well cant cos...our users write imperative, sequence critical software that we cant have unpredictable, behind the back GC...so hah smartpointer, nullpointer kludges. oh LISP has lovely lambdas, why C++ has no lambdas?....well we give u lambdas but please close ur eyes to that horror-syntax; hey we can give static const variables values in the struct/class declaration, why not other variables?...sure that isnt a big problem! and the horror of all...C++ so proud of be a type-safe language yet modern programming is all about type-free so hah lets give u auto! what if i used auto as a var name in my code. tough luck. gotta change it dumbo. u are a bad programmer to make a variable name with just 4 letters.
C++ is so thorny, every symbol impregnated with meaning. walking on egg-shells, no wonder they call it 'cognitive load'.
Every argument you can cobble together against learning the modern language applies as much to C++03, or '98, or, indeed, C itself. And to programming at all. Ultimately, though, it all amounts to laziness.
So the final position is to stop writing any code, and leave the work to those willing to do the work.
i'm done with learning further in C++. C++03 is enough for my purposes. for other features of C++, id use a scripting language integrated into my C++ app say Lisp, TCL, Python or Lua, a lot cleaner altho a bit slower.
Then you had a syntax error. This is a historic keyword that played a useful role in Dennis Ritchie's B and NB languages, predecessor to C. It was retained as a reserved keyword in C, and into C++.
The use of auto in relation to type inference in modern C++ is a repurposing of the existing useless keyword that has always been there.
but i like to see the types im working with. auto like another poster has said though makes it readable kinda hides the type which idiomatically / stylistically i'm not comfortable with.
Use ccache, ninja, the mold linker. Lean into asysnc. Split off libraries. A little attention, once, saves time all day every day.