Ironically, if one wants all of those things, the best alternative at the moment may be the C language.
Ironically, if one wants all of those things, the best alternative at the moment may be the C language.
I play around with new system languages by implementing parts of database engines (typically written in modern C++) in the new language. It terms of abstract code architecture, you can port a C-style database design with relative ease -- most new system languages aim to be an improved C with direct portability in mind -- but not modern C++-style database design, and the latter is unambiguously superior for performance and correctness when writing database engines. C++ will continue to see a lot of usage as long as it is difficult or impossible to write code in other languages that is functionally equivalent to C++.
In C++-style designs, you end up writing surprisingly little code, having metaprogramming scaffolding generate most of the code for you while doing fairly deep correctness and type safety checks at compile-time. Someone has to write the scaffolding libraries but they aren't that large, just tedious, and they get reused. Resource management, change detection, etc is automagic because C++ makes that easy to hide even in complex cases like DMA I/O. Every data structure and algorithm is highly optimized for the local use case and using the most highly compressed representation reasonable in context. It would be impractical to write all of this code and analyze it manually.
I used to write databases in C99. It required several times the lines of code relative to C++17, with worse results, even if you include the scaffolding libraries. C++17 implementations, done well, is much closer to writing a specification for a subsystem design and behavior and having the compiler generate an optimized implementation for that specification and exporting types that hide the fiddly details that can be safely composed with other generated types, than writing code. The scaffolding is also generic and flexible: some can generate an OLTP database engine just as easily as an OLAP database engine largely by changing the subsystem specifications and composing things differently. This would not be possible without heavy use of the metaprogramming facilities to both generate the types and guarantee that type interactions will still be safe and reasonably optimal.
One thing to understand is that these libraries are highly opinionated about the abstract architectural model. It doesn’t make a lot of sense to mix components from user space and kernel space designs, for example, though both have advantages separately. It tends to be more along the lines of one abstract architectural model and enormous amounts of elasticity and flexibility within that model based on the data models, workloads, transaction semantics, and hardware you are targeting. You also still have to write a spec that makes sense from a database engineering standpoint.
At least for me, there are still significant parts of a database engine for which I haven’t built a metaprogramming scaffold. That is largely a matter of time and effort. Other parts I haven’t had to write much code for years but still get state-of-the-art implementation to spec.
Ultimately, I’m trying to automate away my job.
Compare to writing a database system in something like Go. Sure you make end up with 50% more code, but you could have anybody up and running reading and understanding the code within 3 days.
IBM have done studies of this and found that fancy code is not all that valuable. It ends up falling in disuse over time as people don't get it. I have seen my fair share of C++ code which simply had to be tossed because nobody at the company could understand what the previous whiz kid had written.
You can’t write a comparable database engine in Go, fundamentally. The language lacks features required for competitive performance. The code difference will be much more than 50% trying to get the most out of what Go is capable of in this domain.
The point of writing code this way isn’t to be clever or for a “thrill”, it objectively produces superior performance, reliability, and maintainability. Defects scale with the number of lines of code regardless of language. Type safe code gen is a powerful tool.
What are the specific C++17 features that make this a reality?
For C++, the overarching criterion for almost any language feature is how useful it is for capturing semantics in an easy-to-use, performant, powerful, and generally usable library.
The consequence is that using new core language features in non-library code often seems unnecessarily complicated, but a library constructed using the feature as intended is insanely powerful.
The result is that you can write libraries in C++ you cannot write in other languages, and you can call into libraries that are so powerful only from C++.
In most other languages, using a library means you give something up: usually performance, and often it comes with restrictions. The standard of usefulness for C++ libraries is very high, and always increasing.
Something similar happened with coroutines, although what we did get has hooks that a library should be able to patch into, to achieve wonders not yet seen.
Switching to Swift I felt almost everything worked as it should. It was almost a bit boring not having to deal with all the usual crap that C++ would give me. I am willing to be you can write any C++ system much better in Swift. C++ would probably have a performance edge, but in 95% of cases not enough to be worth dealing with an ugly language like C++.
> C++ will continue to see a lot of usage as long as it is difficult or impossible to write code in other languages that is functionally equivalent to C++.
I would challenge you to give me any language construct that is indispensable for a particular type of program which cannot be done more elegantly in another language and with less headache.
I cannot think of a single feature in C++ which I have ever missed in any other of my preferred languages.
If you can’t think of reasons to use C++, that isn’t because reasons don’t exist.
It seems you're complaining about the outcome of poor and ill-advised engineering practices instead of a programming language.
And your description also covey's the idea that you had people who knew very little about C++ trying to figure out how to use it in ways that they have no idea was possible to use.
I get the appeal of a good scapegoat. Yet, from your description it seems you're trying to dump the blame on a lot of engineering problems you're creating for yourself on a tool.
As you identified yourself, that's not entirely true, because C offers all of those things. But the really tragic thing about the status quo is that because there is so much sunk effort behind the C and C++ ecosystem, the resulting momentum makes it difficult for new languages that are simply better to gain traction.
In a way, C++ is the ultimate demonstration of how important the surrounding tool, library and developer ecosystem is to the success (== usefulness in practice) of a programming language. If the language itself were the dominant factor, the writing would have been on the wall when almost every language at a higher level than C was adding features like first class functions and C++ was trying to do something vaguely similar with overcomplicated binder templates.
A language with nicer syntax for working with higher-order functions, Haskell say, might have used
xs = [1, 2, 3, 4, 5]
n = count_if (<3) xs
But for many years in C++, the closest theoretical equivalent would have been to write something like xs = // some standard container type, tediously constructed
n = count_if(xs.begin(), xs.end(), bind2nd(less<int>(), 3))
instead. Obviously hardly any real programmers ever did that, and it's true that modern C++ is better in several relevant ways, but it's been literally decades and it still hasn't entirely caught up.Meanwhile, several much more promising languages are struggling to break into the kinds of markets they deserve to because they haven't achieved a critical mass of support. And the world continues to suffer the loss of productivity and problems with security and reliability that come from using a language like C++ for things that do actually matter. It's an understandable situation, but still a regrettable one.
You're right, I contradicted myself there a bit. Fixed. But C is almost a subset of C++, except for some technicalities. So it's a bit hard to call it an actual alternative. Switching from C++ to C is more or less a matter of restricting oneself to less functionality (which in some cases is a good thing, but I digress).
Other than C it's hard to point at a solid alternative for many C++ use cases.
> In a way, C++ is the ultimate demonstration of how important the surrounding tool, library and developer ecosystem is to the success
Indeed! This is something that any language wanting to compete with C++ must get right. It's a hard thing to do.
I suspect it's actually an impossible thing for a new language to do on its own, because it's not really a technical problem in the first place. It needs a new language with a "killer feature" and serious resources backing it to break through. Several of the relatively success new(ish) languages have combined those two attributes, with the language being in some sense the favoured one for writing software that runs on a certain platform.
People should just accept that they work with a legacy language and not try to turn C++ into Haskell, Rust or something it isn't. They just turn it into a worse mess than it already is.
Most C++ code that exists and which is useful is written in old school C++ anyway and could be continued to be maintained that way and wrapped for other users.
And really C++ performance is IMHO somewhat overrated. It depends entirely on what you are doing. I think it was the CouchDB creator. He made his first version in C++. He struggled hard. Then he switched to Erlang, despite Erlang running on a VM he got magnitudes higher performance and had to write less than half the code.
You know the ones who squeezed the most performance out of the Playstation 2 did it using LISP and not C++. They used LISP to create a DSL for PS2 assembly code. Thus they could do high level LISP coding as well as low level assembly all in one.
And today you got many scientists needing high performance computing switching to Julia. Fortran will usually outperform C++ on number crunching. And there are quite a number of cases where Julia will outperform Fortran.
Yes in real time systems with tight memory requirements something like Julia is not a good choice. But then again in those cause you may actually want to prefer C or Rust.
Gradually porting like this lets you keep using your code base whilst introducing new or overly complex stuff in a language that's faster and easier to write (usually ends up with less than half the LoC of the equivalent C++ but often way, way less than that). Metaprogramming is also very nice, easier to reason about and perhaps most importantly, even with lots of macros doesn't noticeably affect the fast compiles.
Another advantage is once you have some Nim code you can choose to change the target to C or ObjC (or even JS or LLVM) so you're actually increasing portability.
This all depends on how you rank 'maturity' of course. Nim's been around for longer than Rust and Go IIRC, and it's been rock solid for me but you may have different parameters. It certainly helps being able to directly use libraries for C and C++ if you can't find an appropriate Nim implementation.
Edit: For contrast, consider the challenges in the article for variants in C++, then the equivilent object variants in Nim:
type
MyVarKind = enum mvkNumber, mvkString
MyVariant = object
case kind: MyVarKind
of mvkNumber:
num: int
of mvkString:
str: string
var myVariant = MyVariant(kind: mvkNumber)
myVariant.num = 1
myVariant.str = "Oops" # Error: 'str' is not accessible using discriminant 'kind' of type 'MyVariant'A guide to debugging and profiling: https://nim-lang.org/blog/2017/10/02/documenting-profiling-a...
Guide for working directly with GDB-Nim: https://internet-of-tomohiro.netlify.app/nim/gdb.en.html
A walk through and extra info with the language devs: https://www.youtube.com/watch?v=DmYOPkI_LzU
However GDB shows the name mangling suffix in generated code, and types are their native types. There's a script called nim-gdb to add pretty printers for types to the GDB output to show the Nim source types.
Perhaps surprisingly though, the C and C++ generated output itself is fairly straightforward, even with name mangling suffixes. The inserted directives tell you the Nim source line so you can navigate it fairly well if you want to, and the suffix means the variable is unique referenced in the code. As far as I know you can use any debugger that supports the target language, though I've not tried anything but GDB myself.
It's very rare for me to dig into the generated code but sometimes I'm curious about the data structure analog in the target language. In the case of Nim's object variants, last time I looked when compiling to C they were ultimately reduced to simple checked union types.
It's worth mentioning that by default all types in Nim are stack allocated, and you have full manual memory management to the same level as C/C++ but with better type safety and less boilerplate. The GC is only used when you tag a type as `ref`, in strings, and the 'vector' dynamic list type, `seq`.
The newer GC, ARC (not related to Swift's ARC), is similar to RAII - scope based, non-atomic, deterministic, shares memory between threads but not stop-the-world, and uses move semantics: https://nim-lang.org/docs/destructors.html
This makes the GC a nice to use addition for resource management but not a fundamental requirement or speed limitation.
In my experience the default (thread-local refc + cycle collection) GC is very performant already, but it's straightforward to write code that works entirely on the stack, or create objects that wrap manual heap allocs, or use custom external memory allocators. Passing `--gc:none` removes the GC entirely from the compilation target, for example if you're working with very constrained embedded devices with the caveat that less of the stdlib is available (currently).
The ARC GC (doesn't handle cycles unlike it's sibling ORC) is aiming to be lean enough to be used in hard realtime and memory constrained embedded systems. For hard realtime though I'd expect most people would just manually manage their types anyway on the heap or stack.
If you're doing interop between Nim and C++ and want Nim's GC to manage types that you're passing directly to pure C++ code, you can tell the GC that the data is still being used with GC_Ref() or not with GC_Unref(). There are a few libraries for C/C++ interop, such as: https://github.com/nimterop/nimterop
Personally in this case I would probably just manually allocate memory memory in Nim or C++ and not use GC'd types across boundaries for clarity if nothing else, still it's an option if your design requires it.
Finally, there is a tool to help auto-translate C/C++ to Nim with the c2nim tool: https://github.com/nim-lang/c2nim and docs: https://github.com/nim-lang/c2nim/blob/master/doc/c2nim.rst
That's crippling. Arguably the best use case for C++ is coding video games, and portability is huge there.
Of which Rust has backends for all them, even the web via wasm.
While Rust is not as portable, it's not like it's strictly limited to well functioning x86 boxes, it'll produce binaries for anything that LLVM supports, which is quite a lot by now.
Are those backends officially supported on consoles? Do all those consoles have libraries and tools (profilers, debuggers, etc) that support Rust?
We'll see if this changes in the future, especially with some of the names doing Rust dev in these places.
C++14 and C++17 were minor upgrades by comparison, so I'd say C++11 can rightly be called modern C++, though others may disagree.
Of course when it comes to practical portability today then you're right as there are plenty more C++ compilers for different platforms than Rust compilers (then again, there are more C compilers than C++ in that regard). This may eventually change over time, tho, but it sure is something to consider today.
PS and edit: I don't mind you disagreeing with me, but would anybody care to illuminate me as to what your disagreement is about?
https://github.com/conan-io/conan
https://github.com/microsoft/vcpkg
https://github.com/cpp-pm/hunter
Another option if you're on Linux/BSD is to use your native package manager. C++ packages are usually included in those repositories. You also may be able to install Brew, Nix, or Guix on any given distribution.
There goes the portability.
Stockholm Syndrome is more about convincing yourself that a bad situation is good because you're stuck in it. I don't think there are too many people "stuck" with Rust yet. On the contrary, I think the borrow checker is a draw.
sounds like C# and Java with only performance being questionable, but Java's used in HFT, so it definitely can compete.
This is a bad argument and if you're using Java, you're not competing in the very speed-critical parts of HFT. Nor is Java common among HFT shops at all.
well-written C++ is much faster than Java.
So saying that
> Nor is Java common among HFT shops at all.
Sounds weird, especially when it appears among job postings.