I’ve said it many times: a pure C++20 standard library imagined from first principles with total disregard for legacy code would do amazing things for the reputation and value of C++ as a language. It could be so much better than it is.
Yes, I understand, compatibility. At some point, clean new code bases should not be burdened with that albatross.
jeroenhd · 5h ago
I like SerenityOS' "standard library". It has some Rust-like features (like ErrorOr, which is a lot like Result) but also some C++ usability enhancement that make use of modern C++ features rather than trying to force a C++-shaped peg into a POSIX-shaped hole. The only exception I know of is the pledge() implementation (which uses a space separated string rather than something like an array of enum values), but having pledge() at all is a pretty major improvement.
SerenityOS' original "make no use of external libraries" approach made one of the more complete re-imaginings of the C++ application landscape I've seen. It really changed my perspective on what C++ could be.
>The reason it caused the division at community is that the ones that you could not use both on the same project, this meant that for a while you had two "D" languages that were separate, libraries made with one did not worked with the other...
prydt · 6h ago
Have you seen libkj [1]? I've used it and really enjoy working with it. It has a rust-like owned pointer and the whole library uses these smart pointers.
It has proper container classes based on B-trees and its also got an async runtime.
Compatibility is not a reason for not attempting to realize your proposal. (One just needs a difference namespace from "std" e.g. "lib".)
monkeyelite · 8h ago
The problem with C++ is not a bad standard library. It’s probably one of the best algorithms and containers libraries in any language.
platinumrad · 7h ago
<algorithm> is a very good header, but most of the containers are substandard at best. `std::unordered_map` is required to be node-based, `std::map` can't be a B-tree, MSVC's implementation of `std::deque` is infamously a glorified `std::list`, and so on.
Pretty much everything else (e.g. iostreams) is horrible.
TuxSH · 3h ago
Don't forget <tuple> not mandated to be trivial when it can be (and in fact never being trivial with GCC's stdlib), std::print performance issues (see P3107R5, P3235R3), etc.
Heck, even std::atomic was designed with only x64 in mind (it clearly shows), and is unusable outside it. One is incentivized to write their own "atomic" class until P3330R0 is approved for RMW-centric platforms ISAs like Aarch32 and Aarch64.
And of course, Rust already has "fetch_update"...
spacechild1 · 2h ago
> Heck, even std::atomic was designed with only x64 in mind (it clearly shows),
It certainly wasn't.
> and is unusable outside it
Total hyperbole. It's perfectly usable on ARM and other platforms.
P3330R0 looks a nice addition, though.
TuxSH · 1h ago
> It certainly wasn't.
The idiomatic way to do RMW (outside simple stuff like fetch-increment) with std::atomic maps 1:1 with x64 assembly and since fetch_update isn't provided, it's the only way to do it. It's way too close for comfort. See [1] for a comparison
> Total hyperbole. It's perfectly usable on ARM and other platforms.
It's not hyperbole. std::atomic is portable, but that's all it is.
std::atomic is about 30% to 40% (with outlined atomics on, which is the default) slower than handrolled asm (or custom reimplementations that provide fetch_update -- same thing). See [2] for a benchmark.
> probably one of the best algorithms and containers libraries in any language
Mostly agree about the algorithms. Another good thing in C++ are low-level mathematic parts of the standard library in <cmath> and <complex>.
Containers are OK, but neither usability nor performance are IMO great. Node-based containers like red-black trees and hash maps come with a contract which says pointers are stable, by default this means one malloc() per element, this is slow.
However, there’re large areas where C++ standard library is lacking.
File I/O is questionable to say the least. Networking support is missing. Unicode support is missing, <codecvt> deprecated in C++17 because they found out it no longer implements current Unicode standard and instead of fixing the standard library they dropped the support. Date and calendars support only arrived in C++/20. No built-in way to represent money amount, e.g. C# standard library has fixed-size 16 bytes type for decimal floating-point numbers, Java standard library has arbitrary-precision integers and decimals.
simonask · 56m ago
The problem with C++ is not _just_ a bad standard library, but it's also that. There's a lot of "don't use this", including iostreams, std::regex, std::unordered_map, std::variant, and more. Not to mention vestigial parts of failed designs, like std::initializer_list.
Every serious C++ project worth its salt includes additional dependencies to patch it up, and it looks like that will be the case in perpetuity, because these problems are unfixable in the holy name of ABI stability.
Don't get me wrong, ABI stability is a worthy goal. The committee should just have realized that the current approach to it is untenable. The result is a very half-baked situation where ABI stability is not technically guaranteed, but nothing can be fixed because of it.
What a mess.
Rust takes a much, much more cautious approach (because of C++'s history), including explicitly not supporting Rust-native ABI stability and flat out discouraging dynamic linking. Also not very great, but it's sensible as long as there are no clearly superior solutions.
Not even close, and I am a fan of abseil. The design of abseil has legacy constraints that don’t need to apply to a C++20 clean room implementation. I have better re-implementations of abseil components in my own “standard” library simply because I didn’t have those constraints.
There are many good libraries out there, or fragments of libraries, but I’ve never found one that really scratches this itch.
hoten · 8h ago
Could you give a couple examples?
krapht · 7h ago
Of libraries that are better than the STL? Or data structures that the STL is missing? Without getting into the weeds of esoteric data structures and algorithms I think there's a few holes that aren't addressed:
The STL is missing B-trees and B-heaps, as well as d-heaps.
STL is also missing a radix sort, which is even more sorely missed now that we have std::executor::par_unseq to play with.
maattdd · 5h ago
Examples of better design than abseil in a pure C++20 implementation
tialaramex · 5h ago
> proof that the person saying that does not understand the C++ language. This was followed by several "I am a C++ standard library implementer and everyone I know calls it the STL". Things deteriorated from there.
It feels natural to assume that the implementers and long time WG21 members must understand the language, but this is not true. C++ spiralled well beyond the capability of a person to actually understand it years ago.
Maybe that's unfair, but it's unusual. This is a constructed language, its syntax and semantics matter greatly and yet in practice our grasp of its meaning is more akin to that for say, English, than for Perl or Rust.
There are contradictory style guides, people with well meant rules of thumb contradicted by a world of real usage, numerous dialects, not to mention people who strongly believe other users of the language are "doing it wrong" and yet what they're doing works fine.
Compare the C++ code samples with the Modern C++ usually talked about C++ conferences.
This is why I keep saying I only see such high bar code on conference slides, and my hobby projects, those DirectX code samples are much more closer to daily C++ written in big corporations, ironically even those that are C++ compiler vendors with seats at WG21.
On the other hand, there are many industries where I don't see Rust taking anything meaningful away from either C or C++, regardless of how much we complain about them, or how much better Rust happens to be over them.
Const-me · 2h ago
I would not call that example production quality code.
CreateFileA API thing is only available for compatibility with prehistoric software written for Win9x from the nineties. Also, the SDK comes with CAtlFile class which wraps CreateFileW / ReadFile / CloseHandle and guarantees closing handle in the destructor.
They are using a raw pointer in OMMSet structure which leaks memory. I would replace the raw pointer with std::unique_ptr<D3D12_RAYTRACING_OPACITY_MICROMAP_HISTOGRAM_ENTRY[]>
As for the _alloca, I think it’s OK however I usually assert() the size being allocated is reasonable, like under 256kb.
pjmlp · 2h ago
Examples provide the foundation for learning, when the examples are bad, people learn bad practices.
jeroenhd · 5h ago
Microsoft's DirectX C++ example code needs to interact with DirectX' C APIs. That will easily lead to "C with classes" C++ when most of the code is interacting with foreign APIs, like these API demos do.
I think open-source software like https://github.com/microsoft/WSL is probably more representative of what modern C++ companies look like. Plenty of files that just interact with OS C APIs, but no shortage of modern C++ features in use.
pjmlp · 4h ago
That is an excuse, there are modern C++ ways to interact with COM, as shown in C++/WinRT DirectX samples, or using WIL.
In what way does interact with C APIs become an argument to use alloca() with raw pointers?
Just recently I also submitted an issue on Azure C++ SDK regarding use of C style strings and arrays, what is the excuse there?
comex · 8h ago
> Sadly there is not a simple way to integrate this to native loop constructs without macros and even then it is a bit ugly.
I’ve implemented something like this before, without macros. It’s a little ugly, but not that bad IMO.
If you write a native range-based for loop:
for (auto foo : obj) { /* do something */ }
it essentially desugars to
auto it = foo.begin();
auto end = foo.end();
for (; it != end; ++it) {
auto foo = *it;
/* do something */
}
To make it work with a custom type, you need to implement `begin()` and `end()` methods, but the returned objects don’t need to support the full STL iterator protocol; they only need to support the exact sequence of operations from the desugaring. So, for example, `end()` can return a unique `End` type that contains no data and does nothing. `begin()` can return a different type that does all the real work and implements `operator!=(End)`. With that, it’s not too hard to implement a wrapper around a Python-like iterator protocol.
The main drawback is that you need to temporarily store each item in the begin object before it’s moved into the iteration variable. This is because you have to already know whether a next item exists at the point of `it != end`, but then the item isn’t actually retrieved until `*it`. The extra move has a slight cost, but the compiler can often optimize it away to nothing. You can also avoid this if the for loop uses a reference type (`for (auto& foo : obj)`).
112233 · 7h ago
(Since c++17)
The technique you so well describe in your comment does not work with the original range for :(
Not that it is an issue anymore, but I'd forgive anyone who tried to write code like this when range for was added from not trying anymore, because they remember the original semantics.
reads like a testimony in a negligence court case.
tubs · 5h ago
I think you can also define begin/end as non member functions (eg if you don’t own the code for the type).
azhenley · 9h ago
> The post got linked by Hackernews and Reddit. As is usual the majority of comments did not talk about the actual content but instead were focused on two tangential things.
Too true, and this is too good. Start with part 1 (and the comments) if you haven’t.
rzzzt · 6h ago
If you want to read the referenced comments, here they are (probably -- I don't have a crystal ball, only access to a search engine):
Obviously you can delete than iterate. He means delete while iterating.
incrudible · 3h ago
What is the use case for that? Seems more like a footgun, at least for a generic container interface.
simonask · 1h ago
Surely you must be kidding? Inserting/removing in a container while iterating through it is one of the all time greatest and most iconic bugs. People do it because they want to do it.
In reality, very few real-life containers can support this pattern, which is why this is a headline case for Rust, because it statically prevents this bug.
But yes, for removal the correct thing is always to use `std::erase_if` (C++) or `retain()` (Rust). For insertions, the only real solution is to build up a separate collection while iterating and then merging it into the original container when done. Yucky, but won't crash.
Yes, I understand, compatibility. At some point, clean new code bases should not be burdened with that albatross.
SerenityOS' original "make no use of external libraries" approach made one of the more complete re-imaginings of the C++ application landscape I've seen. It really changed my perspective on what C++ could be.
>The reason it caused the division at community is that the ones that you could not use both on the same project, this meant that for a while you had two "D" languages that were separate, libraries made with one did not worked with the other...
It has proper container classes based on B-trees and its also got an async runtime.
[1] https://github.com/capnproto/capnproto/blob/v2/kjdoc/index.m...
Pretty much everything else (e.g. iostreams) is horrible.
Heck, even std::atomic was designed with only x64 in mind (it clearly shows), and is unusable outside it. One is incentivized to write their own "atomic" class until P3330R0 is approved for RMW-centric platforms ISAs like Aarch32 and Aarch64.
And of course, Rust already has "fetch_update"...
It certainly wasn't.
> and is unusable outside it
Total hyperbole. It's perfectly usable on ARM and other platforms.
P3330R0 looks a nice addition, though.
The idiomatic way to do RMW (outside simple stuff like fetch-increment) with std::atomic maps 1:1 with x64 assembly and since fetch_update isn't provided, it's the only way to do it. It's way too close for comfort. See [1] for a comparison
> Total hyperbole. It's perfectly usable on ARM and other platforms.
It's not hyperbole. std::atomic is portable, but that's all it is.
std::atomic is about 30% to 40% (with outlined atomics on, which is the default) slower than handrolled asm (or custom reimplementations that provide fetch_update -- same thing). See [2] for a benchmark.
[1] https://godbolt.org/z/EasxahTMP
[2] https://godbolt.org/z/Y9jvWbbWf
Mostly agree about the algorithms. Another good thing in C++ are low-level mathematic parts of the standard library in <cmath> and <complex>.
Containers are OK, but neither usability nor performance are IMO great. Node-based containers like red-black trees and hash maps come with a contract which says pointers are stable, by default this means one malloc() per element, this is slow.
However, there’re large areas where C++ standard library is lacking.
File I/O is questionable to say the least. Networking support is missing. Unicode support is missing, <codecvt> deprecated in C++17 because they found out it no longer implements current Unicode standard and instead of fixing the standard library they dropped the support. Date and calendars support only arrived in C++/20. No built-in way to represent money amount, e.g. C# standard library has fixed-size 16 bytes type for decimal floating-point numbers, Java standard library has arbitrary-precision integers and decimals.
Every serious C++ project worth its salt includes additional dependencies to patch it up, and it looks like that will be the case in perpetuity, because these problems are unfixable in the holy name of ABI stability.
Don't get me wrong, ABI stability is a worthy goal. The committee should just have realized that the current approach to it is untenable. The result is a very half-baked situation where ABI stability is not technically guaranteed, but nothing can be fixed because of it.
What a mess.
Rust takes a much, much more cautious approach (because of C++'s history), including explicitly not supporting Rust-native ABI stability and flat out discouraging dynamic linking. Also not very great, but it's sensible as long as there are no clearly superior solutions.
https://abseil.io
There are many good libraries out there, or fragments of libraries, but I’ve never found one that really scratches this itch.
https://github.com/martinus/unordered_dense provides better replacements for unordered_map/set.
The STL is missing B-trees and B-heaps, as well as d-heaps.
STL is also missing a radix sort, which is even more sorely missed now that we have std::executor::par_unseq to play with.
It feels natural to assume that the implementers and long time WG21 members must understand the language, but this is not true. C++ spiralled well beyond the capability of a person to actually understand it years ago.
Maybe that's unfair, but it's unusual. This is a constructed language, its syntax and semantics matter greatly and yet in practice our grasp of its meaning is more akin to that for say, English, than for Perl or Rust.
There are contradictory style guides, people with well meant rules of thumb contradicted by a world of real usage, numerous dialects, not to mention people who strongly believe other users of the language are "doing it wrong" and yet what they're doing works fine.
https://devblogs.microsoft.com/directx/omm/
Compare the C++ code samples with the Modern C++ usually talked about C++ conferences.
This is why I keep saying I only see such high bar code on conference slides, and my hobby projects, those DirectX code samples are much more closer to daily C++ written in big corporations, ironically even those that are C++ compiler vendors with seats at WG21.
On the other hand, there are many industries where I don't see Rust taking anything meaningful away from either C or C++, regardless of how much we complain about them, or how much better Rust happens to be over them.
CreateFileA API thing is only available for compatibility with prehistoric software written for Win9x from the nineties. Also, the SDK comes with CAtlFile class which wraps CreateFileW / ReadFile / CloseHandle and guarantees closing handle in the destructor.
They are using a raw pointer in OMMSet structure which leaks memory. I would replace the raw pointer with std::unique_ptr<D3D12_RAYTRACING_OPACITY_MICROMAP_HISTOGRAM_ENTRY[]>
As for the _alloca, I think it’s OK however I usually assert() the size being allocated is reasonable, like under 256kb.
I think open-source software like https://github.com/microsoft/WSL is probably more representative of what modern C++ companies look like. Plenty of files that just interact with OS C APIs, but no shortage of modern C++ features in use.
In what way does interact with C APIs become an argument to use alloca() with raw pointers?
Just recently I also submitted an issue on Azure C++ SDK regarding use of C style strings and arrays, what is the excuse there?
I’ve implemented something like this before, without macros. It’s a little ugly, but not that bad IMO.
If you write a native range-based for loop:
it essentially desugars to To make it work with a custom type, you need to implement `begin()` and `end()` methods, but the returned objects don’t need to support the full STL iterator protocol; they only need to support the exact sequence of operations from the desugaring. So, for example, `end()` can return a unique `End` type that contains no data and does nothing. `begin()` can return a different type that does all the real work and implements `operator!=(End)`. With that, it’s not too hard to implement a wrapper around a Python-like iterator protocol.The main drawback is that you need to temporarily store each item in the begin object before it’s moved into the iteration variable. This is because you have to already know whether a next item exists at the point of `it != end`, but then the item isn’t actually retrieved until `*it`. The extra move has a slight cost, but the compiler can often optimize it away to nothing. You can also avoid this if the for loop uses a reference type (`for (auto& foo : obj)`).
The technique you so well describe in your comment does not work with the original range for :(
Not that it is an issue anymore, but I'd forgive anyone who tried to write code like this when range for was added from not trying anymore, because they remember the original semantics.
The page: https://cppreference.com/w/cpp/language/range-for.html
reads like a testimony in a negligence court case.
Too true, and this is too good. Start with part 1 (and the comments) if you haven’t.
- https://news.ycombinator.com/item?id=43468976
- https://www.reddit.com/r/programming/comments/1jjluxe/writin...
In C++ this is equivalent to an InputIterator.
In reality, very few real-life containers can support this pattern, which is why this is a headline case for Rust, because it statically prevents this bug.
But yes, for removal the correct thing is always to use `std::erase_if` (C++) or `retain()` (Rust). For insertions, the only real solution is to build up a separate collection while iterating and then merging it into the original container when done. Yucky, but won't crash.