Hacker News new | past | comments | ask | show | jobs | submit login
Sane C++ Libraries (github.com/pagghiu)
73 points by cozis 8 months ago | hide | past | favorite | 145 comments



Many of the principles here align with my tastes, such as focusing on fast compile times and supporting allocation failure. On the other hand:

> Unplanned Features:

> SharedPtr

> UniquePtr

> In Principles there is a rule that discourages allocations of large number of tiny objects and also creating systems with unclear or shared memory ownership. For this reason this library is missing Smart Pointers.

I don’t like that at all. I take the common view that all heap objects should at least be allocated via smart pointers. Doing so is safer and easier and usually zero-overhead. After allocation, it may be necessary to pass those objects via raw pointers/references, but smart pointers should be used where appropriate.

So while I agree that it’s undesirable to allocate “large numbers of tiny objects”, I would want smart pointers as long as there’s any dynamic allocation at all.


For me personally SharedPtr is very rarely needed as it encourages building difficult to untangle ownership hierarchies. I did use a lot of Shared Ptr in the past when creating a node.js like library in C++ but breaking the ref cycles everywhere was needed has always been a pain. That's why I am currently against its use, unless there is a very special case.

Regarding UniquePtr<T> I used to have one but I later on decided to remove it.

https://github.com/Pagghiu/SaneCppLibraries/commit/9149e28

However, that being said the library is lean enough so that you can still use it with smart pointers provided by any other library (including the standard one) if that's your preference.


> you can still use it with smart pointers provided by any other library

Is the point of having a kitchen-sink library like this not that you dont have to reach for a 3rdparty library for things that you need 'all the time'?

Certainly, not everyone needs it.

...but, not everyone needs threads either. Not everyone needs an http server; and yet, if you have an application framework that provides them, when you do need them, it saves you reaching for yet-another-dependency.

Was that not the point from the beginning?

unique_ptr is a fundamental primitive for many, as you see from some other frameworks (1), and implementation is not always either a) trivial, or b) as simple as 'just use std::unique_ptr'.

This does seem like a very opinionated decision with reasonably unclear justification; perfectly fair, you're certainly not beholden to anyone to implement features just because they want you to, but I think it's difficult to argue there's not concrete use for something like this, in a way that aligns with the project principals.

I would go so far as to argue that:

> Do not allocate many tiny objects with their own lifetime (and *probably unclear or shared ownership*)

Is hostile to not having a unique pointer.

[1] - eg. https://github.com/EpicGames/UnrealEngine/blob/release/Engin..., https://github.com/electronicarts/EASTL/blob/master/include/...


I usually like to place all dynamically heap objects of type T into an std::vector<T>, if possible. There is sometimes an obvious point where the entire batch should be discarded and a new batch should be built. At this point, you can call vector.clear() which avoids deallocation/allocation cost for the new batch, as long as it is not bigger than the old batch. This is a sort of quick and dirty arena allocation.

This style is also more cache friendly if you are going to be looping through the elements.


I do this too, when I can either use a handle/index instead of a pointer, or when I can guarantee that the vectors size is constant (so that pointers/iterators are stable). I’ve also written my own vector that stores its elements in pages so that if its capacity needs to increase, the elements don’t need relocation.

I only really use C++ for a toy game engine right now and in that codebase I don’t use any smart pointers and most objects/functions get passed references to their object dependencies. I classify objects into groups where each groups ownership is very clear. So its owner is responsible for maintaining the memory and any raw pointers can always be assumed to be borrowed references. I use handles then the underlying objects lifetime might differ from whatever is holding a handle to it. Short lived objects are kept trivial and allocated from stack/bump allocators or pools and reset at well defined times (every frame, end of level, etc)

I’m much happier this way than when I used smart pointers or when I had less well defined memory ownership.


Interesting - is there a type safe way to do this? vector<variant<>>? and/or a custom “vector allocator” to hide the details?


In my example, the T is one specific type. So you could have std::vector<Cat>. If you also have Dogs, you just make another vector std::vector<Dog>. It works fine with the standard allocator. You don't have to do anything special.


Ah okay that makes sense to me


You can do a Vector<TaggedUnion<Union>>. https://pagghiu.github.io/SaneCppLibraries/library_foundatio...

I have not been working (yet) on custom allocators, but that's on the roadmap: https://pagghiu.github.io/SaneCppLibraries/library_container...


If you wanted a factory that allocated vectors of a variety of known types, you'd probably declare template <typename T> before the generator function, so that on compilation a separate version of that function would be emitted for each type you passed to it.

(Not really a c++ expert, but that's my understanding; someone more knowledgeable can correct me).


Why would you need a variant? If you have one of something, put it on the stack. If you have a lot of the a type, put them in a vector.


Isn't that an arena allocator at that point?


Shared pointers are most definitely not zero overhead. The trouble is that they use atomic operations which can cause tons of problems for highly concurrent systems. You can use non-thread safe smart pointers, but at some point you have to ask whether that is really lower risk than just not using shared pointers at all (Rust gets around this with type checking).


Yeah, I prefer RAII over manual memory management. Sorry, strawman.


Adding a bunch of lines like `#pragma GCC poison new` to the last-included header of every source file is very useful. It doesn't fully stop manual memory management from sneaking into headers (since it absolutely will break system headers, though maybe if modules work that would be avoided).

For the rare case of porting software with unclear ownership, I use a `dumb_ptr` template with allocation and deallocation methods. Since this is header-only it naturally avoids the poisoning.

In particular, the `vector` method mentioned elsewhere is completely broken since objects move and thus you can't keep weak/borrowed references to them. If you use indices you give up on all ownership and are probably using global variables, ick. Please just write a proper pool allocator if that's what you want (possibly using generational references to implement weak).


Everything is a tradeoff. Even things like goto, and global variables will occasionally be the right choice.

Regarding the std::vector method, you may have a very loosely coupled system where a bunch of T1's enter into a pipeline and come out as T2's. For this use case, std::vector<T1> and std::vector<T2> are great. On the other hand, if you need to create an object and hand it off to someone else with no knowledge of how long they will need to hold onto it, then std::shared_ptr could be a good option.

In the in-between you have entity component systems that do the type of index tracking you mention so that identities are decoupled from memory location, allowing objects to move. I didn't understand your point about global variables and why they are necessary to implement this type of system. I also didn't understand how this gives up on all ownership. The owner would be the system that maintains the index to memory location mapping.


Why would you implement your own atomics? It seems very similar to std::atomic, except you need to do a bunch of platform specific hacks to get it to work. The only possible reason I could think of why you would do this is if you aren't using at least C++11. If you aren't using C++11, then you probably shouldn't be using threads in the first place.


Also, std::atomic is one of those special types.

It is deeply integrated into the C++11 memory model. The compiler has to know about the semantics of the type to make sure it doesn’t reorder operations around it.


I can't speak for the author, but using `std::atomic` would presumably break one of the project's listed principles:

> No C++ Standard Library / Exceptions / RTTI


Which is like making cookies without butter. It can be done, but why would you want to? The standard library exists for this reason. To provide these abstractions. And I laugh at the idea that C++ could ever be exception free.


I am always baffled by the assertion that C++ cannot be exception-free. The vast majority of C++ code bases I have worked on, at companies large and small, have been exception-free. It is completely ordinary to have no exceptions, and is largely the de facto reality for a lot of C++ development.

In some vanilla app code exceptions are fine, but they introduce nasty edge cases in the kinds of systems code architectures C++ is mostly used for these days. Additionally, they don’t solve an urgent problem in practice that would strongly incentive someone to use them, so it the price of admission is rather steep for minimal benefit.


It isn't compliant with ISO C++ standard library for starters.

I really wish that everyone that plagues C++ library fragmentation with disabled this, disabled that, just stick to C and leave C++ community alone, so that we can fully enjoy the language as designed.


I really disagree with this, C++ is a "choose your own adventure" language, I think this is it's main strength. You decide _everything_, which is conducive to writing systems software at many different levels - 1 language that you can reasonably write HLS+FW+KMD+UMD+libs+CLI+GUI with good support in all cases.


The toolchain and OS I primarily target at my current job doesn't support exceptions. I can stil benefit from all the other aspects of C++ though.


It would make the world a better place if your wish were to come true, even better with <language A> and <language B> replacing C/C++


For me that would be C#, as it is the closest to Modula-3 ideas in mainstream computing, but I doubt many would agree, specially die hard C and C++ devs in some sub-communities, where knowing by heart Assembly opcodes is a reason to be proud of.

I guess we need something else everyone can agree upon.

Until then, the computing foundations will kept be being written in C++, until some big name decides to rewrite LLVM, GCC and VC++ into something else.


C++ was designed?


Yes, as much weird as it may sound to those bashing it.


In the sense that the Chernobyl reactor, the Boeing MCAS system, and the duck-billed platypus were designed, yes.


You can’t guarantee the underlying memory so you can’t guarantee your app won’t blow up. You can handle errors, or expects, or however you want to call “exceptions” without the stdlib but you still have error states, you still have deterministic code branches, you may not have RTTI and non-deterministic std::exception but you still have the concept of an exception. The alternative is to segfault.


You are correct that there is code that solves a similar class of problems as exceptions. This code exists because it is can gracefully handle cases that exceptions handle poorly, in addition to the cases exceptions handle well. The literal C++ exceptions are an inferior tool.

It is trivial to “guarantee” the underlying memory and is idiomatic for a lot of software that cares about performance or reliability. That is code anyone can write if they care. There isn’t much that can go wrong with memory allocation if you are not allocating memory from the system. No one is requiring C++ developers to poorly duct tape a bunch of rubbish STL together and call it an app. That simply isn’t something you see much in the hardcore systems domains where C++ is the tool of choice.

Somehow, mission-critical software is routinely written in C++ without exceptions and it works just fine. Error states are a normal part of all code, no exceptions required since obviously many languages don’t have them. And no, the alternative is not a segfault. C++ is designed to work just fine without exceptions. The language allows you to bring your own error/exception handling models with minimal overhead, same way you can import alternative ownership/safety models.


“Error states are a normal part of all code, no exceptions required since obviously many languages don’t have them.”

An error and an exception are the same concept. Yes, it’s true that not using the std::exception class or any template named “exception” is exception free code. But you’re just lying to yourself. An error check or an assert or verify not <condition> is a “catch”.


No, they're not the same concept, they're both ways to express error conditions but they way they function are very different. People very intentionally do not use exceptions because of how they function and the potential impact on performance compared to other ways of error handling. As well as having other negatives, which other people below have listed when you made a similar comment.


You can definitively use C++ in exception free way. I've been working on multiple Exception free codebases for work.

One large open source project that is using Exception free C++ is SerenityOS for example https://github.com/SerenityOS/serenity

The best way to write proper exception free C++ is not to use the C++ Standard Library.


No, instead of using std::exception, serenity has ErrorOr template. C’mon, that’s basically the same thing. Whatever you want to call it, ErrorOr, OptionalWithError, Exception, you have branching code paths that deal with null/undefined conditions vs your known happy path. Exceptions / Error Handling / it’s all the same class of crap.

Only in C++ land do developers delusion themselves into thinking their way isn’t this way. That exception free means just simply not doing try/catch. jandrewrogers made a good argument below about memory safety and allocations in regards to mission-critical code but even in that scenario, underlying memory can be manipulated by MITM or other conditions that could cause corruption or segfaults in allocator pages.


> No, instead of using std::exception, serenity has ErrorOr template. C’mon, that’s basically the same thing.

I think that handling errors with ErrorOr<T,E> or similar techniques (I use something similar too) is very different from exception handling.

My main problems with exception handling are:

1. It's not zero-overhead (brings in RTTI often) 2. You can't know if a function throws something by looking at its signature 3. You don't know what types exceptions a function can throw 4. It doesn't force users to handle errors that can happen, leading tomore "happy path" style code

Something like ErrorOr or similar with [[nodiscard]] ticks all the above 4 points.


If the argument is purely don’t use std::exception if you don’t want the overhead, use MyExceptionTemplate because it doesn’t introduce overhead then sure, I’m for that. If the argument is don’t use it because it’s non-deterministic, use MyErrorWrapperMagic, fine. But in the software engineering sense you are still exception handling, only deterministically - which I like. Everyone is getting hung up on semantics of std::exception instead of exceptions. Exceptions are a part of everyday coding. Why we call things exception-free when we really mean “goto-free”. I’m not a fan of goto jumps in my code.

ErrorOr is a better design for things to be deterministic. The point I was trying to make is that exceptions are errors and error handling and exception handling (while implemented differently) are essentially the same thing. In C++ std::exception is “exceptions” and everything else is “pretty error”. It doesn’t matter how the house is decorated. Exceptions = Errors = Oopsies = NotIntendedState


> C’mon, that’s basically the same thing

I'm not going to talk about errorOr specifically because I don't know how it's implemented but rather your premise.

Exceptions are the modern goto, the try catch may be in the caller function. Or it could be 3 inherented classes away (this is hyperbole, I'm not sure if it would actually work) with so much indirection it would be impossible to follow the code flow other than to step in a debugger. So in fact it's worse than a goto since at least a goto had a label.

I'm not for or against exceptions just pointing out that a result or optional type is in no way at all similar to an exception.

On another point C++ exceptions are notoriously inefficient as well. There ware valid reasons to want to be exception free even from a code style perspective.


I'd argue that pubsub systems are the modern goto, but exceptions certainly come in a not too distant second.


When you wrote pubsub, did you mean “the observer design pattern”? If so, please try to adopt a more formal lexicon and use terminology familiar to more developers. If not, please describe what a “pubsub” is.


No, I don't mean the observer pattern. I mean publisher subscriber pattern.

Observer pattern is fine, because there is still a link between the execution flow; basically any time you register a callback you're using the observer pattern.

Pubsub adds a middleware, typically in the form of a message passing framework, so that both publisher and subscriber aren't necessarily aware of each others existence. The publisher throws its data into the framework hoping someone finds it useful, and the subscriber listens for those messages hoping someone is publishing them. Think of it like multithread asynchronous goto of your execution flow, except with a lot more boilerplate.


Wait I need to hear more about this.

How are pubsub systems a modern goto? Genuinely curious since I don't work with them directly that often


If you give me a high level explanation about why goto is bad I can basically /s/goto/pubsub and it will still hold mostly true.


> why would you want to?

It seems like you know C++ pretty well, so I think you could probably come up with some reasons if you gave it a try.

The obvious thing that comes to my mind is because you can't rely on an undesirable, unknown, or potentially nonexistent implementation of the STL for your target platform.

As for cookies: who am I to dictate what/how someone should bake?


As a person working in a large exception-free C++ environment, I'm all ears.


I assume you're making use of no-throw placement new and not touching any standard library type, whose error conditions as available only via exceptions as per ISO C++.

Otherwise good luck porting that exception free code across multiple OSes and C++ compilers, not only the big three.


What standard library? ;)

There is a "turn code into object file using compiler" C++, that is much different from "committee vision" C++, and until recently (probably until initializer lists), -fno-exceptions -fno-rtti -ffreestanding did not emmit standard library symbols, except maybe static initialization.

Current C++ is being glued to library like crazy, look at continuations...


The one defined by ISO/IEC 14882 in a vault in Geneva.


Presumably they just use -fno-exceptions, and not everyone has to have portable code. I appreciate the concept but pragmatically there are plenty of C++ server side projects that only use Clang and GCC


“-fno-exceptions“

Here-in lies the problem. This only says “no std::exception” “no throw” but nothing is stopping you from writing your own template for error handling. An exception is by definition a “unexpected condition” and we all have to handle exceptions in some form or fashion. Whether that’s with error codes, macro checks, or the like. That’s my point. Exception free code doesn’t exist. Safety guarantees are compile time at best. If this weren’t true, things like memory manipulation wouldn’t be possible. But it is. CheatEngine exists. Funnest thing is to read a var, set a var, and then check if the set var equals the set value or if it equals the old value. You just detected a memory pin cheat.


I think you're terribly misguided here; when people discuss "no exceptions" here, they do not mean "any error handling that is not the typical/expected flow", they mean "getting rid of built-in exception system that comes with the heavy price of stack unwinding and the related mechanisms", not that they do not want to handle errors.


Yeah, and then come into twitter complaining when the code fails spectacularly after a compiler upgrade.


Exactly!

This is all in an effort to bring down compile times, avoiding including anything from the standard because you can never know if including <atomic> is bringing 10K lines of code in your header.

I will probably provide an optional USE_STANDARD_HEADERS flag someday to allow including a few standard things, including atomics, to avoid doing things wrong on compilerS that are not tested enough (as I clearly can't test every compiler).


Looks like the author likes C but would like a few of the features of C++ and wants to have some fun. There’s noting wrong with that, but it ain’t for me.

The stuff he considers complex is mostly complex because it handles a lot of corner cases, or else it has back compatibility constraints he will be dealing with as well.

Good luck to him though. Doing stuff for fun is the best!


I honestly like well written C a lot :)

And yes, complex stuff sometimes tries to handle "everyone's use case" but if you can limit yourself to 95% of use cases, your code suddenly become a lot simpler. The backward compatibility consideration holds true as well.

For example, I have been creating an Async Library (plus a few other things like the FileSystemWatcher etc.) that cover a good portion of what is done in libuv. Of course libuv code handles A TON more of edge cases and has a lot of compatibility constraints, but with a lot less code I can provide enough functionality to satisfy a lot of use cases. Not all use cases, but a lot of use cases.

Thanks for the good luck! I am definitively doing it just for fun :)


I've come to despise how tightly integrated the C++ standard library is now with the c++ language itself, it is clearly evolving in a direction to make the two inseparable, but it is also making it less desireable. One cannot even compile a c++ executable with the /NODEFAULTLIB flag and not break core language features, e.g. static global object construction, dynamic_cast, ... For the purists that would like to separate the standard library from the language, and make tiny executables not depending on the runtime library, C is the only choice now it seems.


This is what “freestanding” is for, FWIW. There are a variety of papers and implementations around this goal, which is generally to have a set of features of c++ the language and the standard library that can work entirely unhosted.

I realize people think of this as a difference between c and c++, but if you go to compile C code with a thread_local variable and throw -nostdlib you’re going to have a bad day. Same for atomics, sometimes complex numbers, floating point exceptions, even receiving arguments and other core language features require some crt code. Removing the runtime library guts the implementation regardless of your language. The question is, does your language provide ways to deal with this? Rust has core, c has alternate ad-hoc embedded libc implementations and a history of bootstrapping implementations long enough to have them be well understood, c++ has/will have free-standing.


https://github.com/nlohmann/json

I used this for JSON last time I wrote any C++ a few years ago and it still seems popular. It seemed sane enough to me.


Boost.JSON is arguably the better choice these days. It's roughly as usable as nlohmann but faster than RapidJSON

Overview: https://www.boost.org/doc/libs/1_84_0/libs/json/doc/html/jso...

Benchmarks: https://www.boost.org/doc/libs/1_84_0/libs/json/doc/html/jso...

Parsing Options for non-standard JSON: https://www.boost.org/doc/libs/1_84_0/libs/json/doc/html/jso...


> No C++ Standard Library / Exceptions / RTTI

This alone already rules them out as sane on my book.


No stdlib or no exceptions/RAII. Because everyone at Google is on the no exceptions/RAII train (according to the Google Style Guide), and I happen to like exceptionless C++


Just because it is Google, doesn't mean their style guides have any quality value.

Any company that worships that style guide is one I am happily never going to work on.


You mean RTTI instead of RAII, right? Google definitely uses RAII.


Yes RTTI


'Sane' C++ is apparently still using macros all over the place. :) Not really to be taken seriously.


The majority of the macros are SC_PLATFORM_XXXX to inject platform specific code here and there. There are macros in the Reflection library but you have the option not to use them.

What macros are bothering you the most?


Interesting. But I think the vast majority of this functionality is already available in the Qt framework, which I use for pretty much all my projects.


Of course Sane C++ Libraries targets a much much smaller functionality subset than Qt (that is a good library in many ways) and of course it has orders of magnitude less complexity. You can use Sane C++ Libraries adding a single file to your project for example. Also, Qt used to have an LGPL + Commercial licensing scheme (not sure how this has recently evolved), while this project just MIT.


There probably isn't a compelling use case for Qt-philes like me. But I wish your project the best of luck!

(the community version of Qt is LGPL, I have a small business licence)


I don't think describing the build in C++ (https://pagghiu.github.io/SaneCppLibraries/library_build.htm...) can exactly be called a "sane" idea.

Declarative build definitions are generally much easier to work with and scale than using an imperative language.


I could bring multiple examples, but just to make one CMake, what I think today is the most popular way of describing builds in c++, describes builds in an imperative language.

https://en.wikipedia.org/wiki/CMake#CMakeLists.txt

Most "declarative" build systems are not actually what they "declare" to be. I've seen too many DSLs introducing half backed imperative concepts here and there to do _if_ and _for_ constructs or function calls, redoing the same as imperative languages but poorly.


Actually this sounds useful as alternative to C++ stdlib: i've often compiled C++ code via GCC for simple stuff where C++ stdlib isn't included by default and all 'nice' C++ things are not linked, giving less overhead per file, but forced you to rely on ancient C functions. This would be a middle ground solution between unsafe C and bloated stdc++.


I don't like to market it as an alternative to C++ stdlib, also because it doesn't cover all the things done by the C++ stdlib (in particular regarding Containers and Algorithms, as noted in other threads on this discussion).

I like to market it as an "alternative world" where the C++ stdlib is more a platform abstraction library focused on carrying practical tasks like networking, Async I/O, HTTP etc.

It's also definitively placing itself in the middle between unsafe C and bloated C++.


Author here! Feel free to ask me any question, here or on discord/X/Mastodon I just saw this posted here, wow :)


Just use C and guarantee your sanity. The whole reason to adopt C++ is to support huge projects and libraries all of which are going to use the “bad” stuff.


That's actually an excellent advice! :D

I love well written C libraries, like the sokol or stb libraries.


Looked at the first library in the list, "Algorithms". The first item in that library is bubbleSort(). I'll pass.


Yes, the Algorithms library is just a placeholder, as specified in the docs.

https://pagghiu.github.io/SaneCppLibraries/library_algorithm...

Hopefully it will get expanded with more useful algorithms, it has not been a priority in the first releases cycle.


What is the point of `algorithms::bubbleSort`?


Last I checked bubble sort was the fastest sort for small counts of small objects. That was 10 years ago though so thing might have changed potentially something that might take better advantage of vector ops.

It's also often the correct choice for something like a collision partition for a physics sim where elements are most likely in the same sort position each frame.


Around half as much code is generated for bubble sort compared to std::sort, e.g.:

https://godbolt.org/z/KbaTeno3j


how does it compare to https://abseil.io/


Abseil is more of a complement to the STL rather than a complete replacement like the OP is, for example the only containers that Abseil provides are maps and sets since the STL ones are particularly bad, and the rest of the STL containers are good enough (for Google at least). Whether that's the right approach depends on what your specific qualms with the STL are.


POCO is another solid collection of C++ libraries with similar objectives. It looks as if POCO is more mature.


> It looks as if POCO is more mature.

POCO started 20 years ago, so it has a slight advantage :)


The first library I checked was "Reflection". This is where I can see if their principle holds up


Of course I would have been doing the same :)

The documentation here states:

https://pagghiu.github.io/SaneCppLibraries/library_reflectio...

Note Reflection uses more complex C++ constructs compared to other libraries in this repository. To limit the issue, effort has been spent trying not to use obscure C++ meta-programming techniques. The library uses only template partial specialization and constexpr.


Still no 3D renderer

TinyEngine is good, but doesn't build on windows yet


Well that would be slightly increasing the project scope


The lack of STL containers or even a fully implemented set of custom containers, to me, makes this kind of a non starter. I use C++, in part, because I don't want to have to implement Data structures and algorithms myself


That's a fair observation.

What containers, beside Vector<T> (and Map<K,V> made with Vector) + variants would you like to see the most?


Set, Stack, Queue, and their various implementations (HashSet, PriorityQueue, etc)


There is a VectorSet that creates Set with an unsorted vector. I think it would be good (and easy) creating a SortedVectorSet for better performance.

HashMap and proper Map<K,V> are already on the roadmap https://pagghiu.github.io/SaneCppLibraries/library_container...

Stack can be easily created with Vector.

I think Queue is pretty specialized, but I will think about it.


My projects use stack and deque often.


Stack can be easily created with Vector (I can add it, thanks for the hint). I am conceptually against using Deque. If you need to keep stable addresses for objects you can use ArenaMap https://pagghiu.github.io/SaneCppLibraries/library_container...


I'm unsure about the asserts instead of errors


As someone who doesn't write C++, why does almost everyone seem to insist on ignoring the STL and writing everything themselves? Both C++ and C# take a "bags included" approach to the standard library, but no one is writing their own `List<T>` for C#.[a] Yet, everyone seems to have their own opinion about `std::vector<T>`.

[a]: Obviously, people still write their own collections/containers in C#, but they tend to only do so for very specific/performance-sensitive circumstances.


Because C#, at several points in its history, sensibly took the decision to explicitly break backward compatibility to satisfy legitimate requirements, adapting and evolving to the tenets of modern software practices. (Generics and new collections)

Whereas due to C++'s "never break compatibility" decision, the standard library has progressively decayed over time. It has become a bloated, rotting dinosaur where even the slowest of interpreted languages can comfortably beat several of its aspects. (Ex: std::regex is pathetic and pitiful, vector<bool> triggers laughter, substandard maps, etc). Considering that C++ thumps its chest and loudly proclaims its superb performance, this has now become a sad joke.

In the natural world, a species that cannot adapt to new circumstances and never discards undesirable characteristics simply perishes.

The C++ Standard Committee has firmly and unequivocally decided that the C++ language should mirror the same approach and limp down the road, carrying the full-weight of its sins for all its journey, until it falls into oblivion.


Note that as documented in the history of F# HOPL paper, .NET already had a generics prototype before 1.0 release, Microsoft decided to go ahead without generics to avoid delaying it further until they were stable.

Also breaking everything a couple of times is why most corporations are nowadays stuck in a Python 2 / 3 parallel world in .NET Framework / .NET Core , or in Xamarin.Forms / MAUI, UWP / WinUI,...


There are so many reasons. I'm sure others will provide their own favorites.

One that I find particularly annoying lately: if you work on a project that makes heavy use of the STL (or, really, any heavily templated library written in a similar style, with a focus on "ergonomics" :-( ), you'll quickly find that backtraces for debug builds can easily be 50-100 levels deep with most stack frames just consisting of incomprehensible layers of abstraction which get optimized away. So, debug builds are totally useless, and build times are typically long. Contrast that with something written in "C style" or simply making far fewer use of C++ features, written in a more straightline style, with fewer levels of abstraction: builds will be fast, stack traces will be small and can easily map directly onto the concepts relevant to the program or library, and debug builds are useful once more. Night and day difference.


It does not have to be night and day. In gamedev I saw over years that giving up debug builds for a set of your own abstractions has benefits. You do not have to use STL or exceptions so you can have a balance in your build times. Having abstractions in the code has a lot of value. People overestimate how hard is to do deoptimize on checkout or on demand which solves most of debugging needs.


I appreciate the sentiment of what you're saying, but I guess I'm not totally sure what you mean by "abstraction". Maybe "abstraction" is the wrong wording for my original post... For example, in boring code you can have "abstractions" in the form of data types that model high-level concepts along with high-level functionality on them. I'm not at all opposed to that. The "Sane C++ Libraries" linked seem to be a reasonable example of this style. There are no features in C++ that you absolutely need beyond what's offered in C to do this.

Maybe as an example just compare a library like Eigen to a similar imaginary library written in C.

Eigen leans heavily on C++ features to reduce line count and make something look visually more mathematical or maybe more MATLAB-style in the name of ergonomics; and obviously on the backend it relies on the behavior of the C++ compiler to simplify the elaborate template expressions and make the code efficient. If you ever try to step through code that uses Eigen in a debug build or examine a stack trace from inside Eigen where an exception has originated, the situation is not pretty! Contrast this with what will happen in the imagined C-style library. If all the heavy lifting happens in BLAS or LAPACK and the C library is basically a thin library to make things a little more automatic and easier to manage, the stack traces will be short and each stack frame will be easy to digest at a glance because of names like "mat_mat_mul" or similar, instead of "mat<double, double, 4, allocator<...>> const & operator*(mat<double, double... 159 more characters of template gobbledygook that makes the eyes glaze over)". The former will also be significantly faster to compile.

Anyway, I guess I just don't see how the latter STL/Eigen/whatever-style approach to things is an improvement over the far simpler idiot style of doing things.

I'm not trying to be argumentative here, I just want to clarify my point as much as possible.


yeah, you have it completely consistent.

My point is that one can have a lot of C++ working at scale. Reading code is very important part of large scale projects. Dropping debug builds is a reasonable tradeoff.


OK, got it... I suppose that's reasonable, but depending on what you're working on, I'm not sure seriously reducing the debuggability of your project is acceptable. For the kind of work I do, I need a debugger.

I would also argue that keeping it so that you can always easily run your project "REPL style" from a debugger exerts a very favorable downward force on all the complexities that make large projects hard to maintain.


In decades of professional C++ experience I have never noted depth of backtrace as a usability issue. Give us an example of how a 100-deep backtrace arises as a consequence of the STL.


Agreed, at least for STL containers I didn't really have to dig into the implementation for 99.9% of the cases. One or two specific old cases were due to memory corruption, but nowadays address sanitizer should do the job. Of course, a lengthy symbol for heavily templatized functions/classes can sometimes be a problem, but it's a separate issue from stack trace depth.


"Functional" C++ with deeply nested loops implemented using stuff from <algorithm> and <functional> and lambda.


Before concepts it was certainly possible, but when it happened you just had to add some extra type assertions to make it clear where the error is.


Take your pick: undercooked functionality, lack of behavior/performance consistency between different toolchains, bloated implementations causing long compile times, inscrutable template vomit error messages and poor performance in debug builds (a dealbreaker for realtime apps like games), and design mistakes which can't easily be fixed, such as std::vector<bool> being a footgun or the way that the standard map types are defined making them impossible to implement efficiently.


The containers in the STL have issues due to the limitations of C++ when they were designed, catering to the lowest common denominator user, and simplifying things for implementors. It is not uncommon to find cases where the STL is a poor fit or annoying to use due to its implementation and design details. In the case of modern C++, you also have the issue that the STL containers aren't designed to be used in an advanced metaprogramming context, because they pre-date that being an intentional part of C++. Some of this is unfixable because backward compatibility. Also, C++ is commonly used in contexts where performance is critical, so the optimality of the STL for purpose matters more.

Some of the common issues: static allocation or lack thereof, requiring default constructible classes, initializing memory you are going to overwrite anyway, inability to be used in some metaprogramming contexts, suboptimal allocation behavior, etc. The STL is opinionated but unfortunately that opinion dates to a time when C++ was primarily used for ordinary app development, not high-performance code.

Like many, I maintain my own C++ "standard library" that is much better designed for the kinds of software I tend to work on (database kernels and data infrastructure, mostly).


Because people tend to write c++ for performance reasons and the perf profiles of std::vector are not always sufficient


The only thing you can really quibble about with std::vector is whether your library has made an optimal choice of growth strategy, which you can often hack around by reserving. Aside from that, access via `operator[]` and growth via `emplace_back` will compile down to optimal code that is going to be close to impossible to beat. After the compiler gets done with it, it looks the same as if you had hand-coded it with arrays in a C-style, but without the plethora of bugs that often results from that approach.


> The only thing you can really quibble about with std::vector is [...]

That's actually not true, though I certainly don't fault you for believing it :-) but there are definitely more things to quibble about around vector if you're serious about performance. As an example, try writing a can_fit(n) function, which tells you whether the vector can fit n elements without reallocating. Observe the performance difference between (a) a smart manual version, (b) a naive manual version, and (c) the only STL version: https://godbolt.org/z/88sfM1sxW

  #include <vector>

  template<class T>
  struct Vec { T *b, *e, *f; };

  template<class T>
  bool can_fit_fast(Vec<T> const &v, size_t n) {
    return reinterpret_cast<char*>(v.f) - reinterpret_cast<char*>(v.e) >= n * sizeof(T);
  }

  template<class T>
  bool can_fit(Vec<T> const &v, size_t n) {
    return v.f - v.e >= n;
  }

  template<class T>
  bool can_fit(std::vector<T> const &v, size_t n) {
    return v.capacity() - v.size() >= n;
  }

  struct S { size_t a[3]; };

  template bool can_fit_fast(Vec<S> const &, size_t);
  template bool can_fit(Vec<S> const &, size_t);
  template bool can_fit(std::vector<S> const &, size_t);


I was curious why the std::vector version produced so much more assembly, so I tried to dig a little. At least in libc++, the relevant code is (cleaned up):

    template<typename T, 
    class vector {
    private:
        pointer __begin_;
        pointer __end_;
        __compressed_pair<pointer, allocator_type> __end_cap_;
    public:
        constexpr const pointer& __end_cap() const noexcept {
            return this->__end_cap_.first();
        }
        constexpr size_type size() const noexcept {
            return static_cast<size_type>(this->__end_ - this->__begin_);
        }
        constexpr size_type capacity() const noexcept {
            return static_cast<size_type>(__end_cap() - this->__begin_);
        }
    };
So if the functions are fully inlined we should end up with

    template<class T>
    bool can_fit(std::vector<T> const &v, size_t n) {
        return static_cast<size_t>(v.__end_cap_.first() - v.__begin_) - static_cast<size_t>(v.__end_ - v.__begin_);
    }
At least algebraically (and ignoring casts) it should be equivalent to v.__end_cap_.first() - v.__end_, which is more or less what the manual implementations do. Maybe the optimizer can't make that transformation for some reason or another (overflow and/or not knowing the relationship between the pointers involved, maybe)?

If you change can_fit(Vec<S>) to:

    return (v.f - v.b) - (v.e - v.b) >= n;
You end up with code that looks pretty similar to the can_fit(std::vector<S>) overload (the same for clang, a bit different for GCC), so it does seem it might be something about the extra pointer math that can't be safely reduced, and the casts aren't really relevant.

(I'm also a bit surprised that can_fit_fast produces different assembly than the Vec can_fit overload)


Kind of, yeah. The main thing to realize here is that pointer subtraction is not just arithmetic subtraction of the addresses; it's also followed by a division by sizeof(T). (This is not obvious in this context unless you've seen it before.) Thus for the compiler to prove that (f - b) - (e - b) == f - e, it has to prove the remainders of each address subtraction (mod sizeof(T)) don't affect the result. This is certainly possible, but it requires compilers to actually do some extra legwork (e.g.: assume these come from the same array, then prove that implies the remainders don't affect the result, etc.) prior to reducing these to arithmetic operations. For whatever reason they don't do that. My guess is the reason is something between "we believe the general case would break too much existing code" and "we believe this special case isn't common enough to be worth it". But who knows; maybe if someone files this as a missed optimization, they would try to implement it. I haven't tried.

(There's also a secondary issue here, which is that sizeof(S) isn't a power of 2. That's what's introducing multiplications, instead of bit shifts. You might not see as drastic of a difference if sizeof(S) == alignof(S).)


Yeah, the extra instructions from the division helped clue me into what might be going on, since the individual (f - b) and (e - b) calculations are visible in Clang's output.

I feel the division by sizeof(T) shouldn't matter that much, since the compiler knows it has pointers to T so I don't think the divisions would have remainders. I want to say pointer overflow and arithmetic on pointers to different objects (allocations?) should also be UB, so I suppose that might clear up most obstacles? I think I'm still missing something...

Does make me wonder how frequently this pattern might pop up elsewhere if it does turn out to be optimizable.


My 1st paragraph was directly answering your 2nd paragraph here (starting from "This is certainly possible..." to the end). I was saying, compilers can optimize this if they want to, but it requires work to implement, and I can only guess (the reasons I listed) as to why they might not have done so yet.

> Does make me wonder how frequently this pattern might pop up elsewhere if it does turn out to be optimizable.

Probably a fair bit, but as I mentioned, it might break a lot of code too, because there's too much code in the wild doing illegal things with pointers (like shoving random state into the lower bits, etc.). Or not... the Clang folks would probably know better.


Right, I suppose my second paragraph was waffling on the extra legwork the compiler would have to do, but I know validating optimizations is just the start of the work.

Maybe this could be a good way to jump into messing with LLVM...

Out of curiosity, how much of a performance difference did you observe in practice when you made this optimization?


I don't recall unfortunately, it's been a few years.


I wonder if you actually measured it with proper compiler options? I saw some folks insist that STL is somehow slow blaming unnecessary layers but many of them are pretty much groundless. There are some genuine spots where STL has a real performance problems, but I don't think your example illustrates it well.


Click the link in my post, I added it so you can see everything in action for yourself.

(Although, note I didn't claim "the STL is slow"... that's painting with a much broader stroke than I made.)


Why would that be faster? Those function calls are going to be inlined.


It's not the function calls they are alluding to, it's the way the compiler generates a bunch of shifts and multiplies except in the can_fit_2 case.


Turns out GCC is more clever than Clang here, and MSVC is just horrendous. (See update, I posted a link.)


I somewhat agree with your point (esp. that MSVC is hideous) but I also stand by mine. I don't feel that checking the capacity is something that would be in my tight loop, because checking for capacity to hold N is something you do before adding N items, which amortizes the cost, meaning the capacity check is just off the real hot path. So it doesn't feel like a realistic use case.

Speaking of realism, putting these in quickbench seems to confirm that the differences between them are not material, and that the STL version is in fact the quickest, but they are all essentially free. There's not a way to make a realistic microbenchmark for this, for the same reason that it doesn't feel like a real-world performance issue.

By the way clang does a much better job here: https://quick-bench.com/q/XcKK782d-7A6YHbiBRTlOnIRnPY


> I don't feel that checking the capacity is something that would be in my tight loop, because checking for capacity to hold N is something you do before adding N items, which amortizes the cost, meaning the capacity check is just off the real hot path.

Your fallacy here is assuming that that just because N is a variable, therefore N is large. N can easily be 0, 1, 2, etc... it's a variable because it's not a fixed compile time value, not because it's necessarily large. (This isn't complexity analysis!)

> Speaking of realism, putting these in quickbench seems to confirm that the differences between them are not material, and that the STL version is in fact the quickest,

Your benchmark is what's unrealistic, not my example.

I'm guessing you didn't look at the disassembly (?) because your STL version is using SSE2 instructions (vectorizing?), which should tell you something funny is going on, because this isn't vectorizable, and it's not like we have floating-point here (which uses SSE2 by default).

Notice you're just doing arithmetic on the pointers repeatedly. You're not actually modifying them. It sure looks like Clang is noticing this and vectorizing your (completely useless) math. This is as far from "realistic" as you could possibly make the benchmark. Nobody would do this and then throw away the result. They would actually try to modify the vector in between.

I don't have the energy to play with your example, but I would suggest playing around with it more and trying to disprove your position before assuming you've proven anything. Benchmarks are notoriously easy to get wrong.

> So it doesn't feel like a realistic use case.

I only knew of this example because I've literally had to optimize this before. Not everyone has seen every performance problem; clearly you hadn't run across this issue before. That's fine, but that doesn't mean reality is limited to what you've seen.

I really recommend not replying with this sentiment in the future. This sort of denial of people's reality (sadly too common in the performance space) just turns people off from the conversation, and makes them immediately discredit (or just ignore/disengage from) everything you say afterward. Which is a shame, because this kind of a conversation can be a learning experience for both sides, but it can't do that if you just cement yourself in your own position and assume others are painting a false reality.


I can't believe you typed all that just to show your condescending, point-missing aspect to its fullest.


I gave you an example from my personal experience in the real world. Instead of asking for the context, a benchmark, or anything else, just went ahead and declared I'm giving you an unrealistic example... surely you can see why this is kind of offensive? I suggested you don't do that, and even explained specifically what appeared off in your benchmark. This is missing the point and being condescending?


There's also the issue that std::vector<bool> is required by the standard to be specialized as a bitset, which is a footgun in generic code since you can normally take the address of a vector element but not if it's a vector of bool. Having a bitset in the standard library is fine but it should have been a seperate type.

Admittedly that's not a performance issue, but it's annoying.


That's true but I think everyone knows about vector<bool> being quirky. By the way, the standard does not require vector<bool> to be implemented as a bitset. Instead, it relaxes some of the details of vector, in a way that allows the implementation to do it that way. But these choices are implementation details.

Vector<bool> is a little weird if you are just starting with C++, but it does have major performance benefits in its niche, and it came from the 1990s so we can be generous in overlooking its rough edges.


It came from the 1990s, but it overstayed its welcome. I remember seeing proposals to remove the vector<bool> special case, what is the situation?


What specifically is wrong with vector? There have been a lot of hash maps done with flat memory to minimize allocations and pointer hopping over the STL but vector doesn't have those problems.


> why does almost everyone seem to insist on ignoring the STL and writing everything themselves?

Not really. Unless you have a very specific set of performance requirements, STL containers are usually more than enough. And we have third party libraries such as Abseil/Boost to cover the major gaps in the rest. I do see some legitimate cases to write own container libraries. But for many cases people don't really measure their primary workload before writing such libraries, instead they just write it because they can write (and it's fun).


Doesn't it contradicts the 'bloat free' requirement, if every library needs to have its own version of std::list std::vector, std::unordered_map etc ?

I mean: i remember the days, when each and every project had its own string class.


The bloat free is more referring to executable size, build complexity, compile time and in general to hidden complexity.

Every library having its own version of common data structure is unfortunately something that C++ programmers can't really seem to agree on :)


The implementations of vector, list, and unordered_map in every standard library I've used have been sufficient for every C++ library I've worked on.

There are special cases, and there are engineering politics, but it's all basically fine.


Because unlike CoreLib, the standard library in C++ tends to have a lot of insanity backed in. My knowledge on it is somewhat limited but looking into its APIs for working with strings (in large inherited from C) was so traumatizing that I only ever wonder why there are still self-respecting projects that don't reimplement parts of STL.

Example: want to measure the length of a UTF-8 code point in a string and did not synchronize the call? Well, too bad, now you might have corrupted the global mutable state it relies on! (the fact that you can make such a trivial piece of code have two!!! points of thread-safety failure one is C locale and another is transcoder still refuses to leave my mind)


Can't believe someone actually went out of their way to downvote this haha


Sounds like you weren't working with C++'s string API. std::string knows nothing about the encoding of the data it encapsulates and can be used to store anything. There are the codecvt functions but it doesn't sound like you're talking about those, either.


It's still funny to me how something titled "Sane C++ libraries" has zero interop with the STL; guaranteeing an even more fragmented C++ ecosystem.

C++ has its place, but something new could not possibly displace it soon enough, even with C it feels like libraries fit together more easily.


Its the very reason C++ is still alive. Its unopinionated on how u code and coding enviroment. Plenty of other language are far more restricted in their ecosystem


I just think things could be a little more aligned as I use C++ daily and often end up interacting with many different libraries and each has their own slightly idiosynchratic String/Vector/etc type, which quickly makes life.. interesting

But I see the point in it helping C++'s unusual longevity as well


Yes the library is trying to model an alternative C++ world where the standard library tries to be more like the standard libraries of other languages (Python, nodeJS for example) providing actual functionality out of the box rather than just "containers and algorithms".


Huh right I see, I do like the idea of that and I wish that is what the actual C++ stdlib was more like, it would make my life of using C++ a lot more pleasant than it currently is :)

(also sorry my initial comment came off like ragging on your library, it wasn't meant that way, it was more of a commentary on the overall state of the C++ ecosystem, so I appreciate people with a slightly broader view like yours!)


Yes, I am trying to make C++ more pleasant than t currently is :) I like Python and JS ecosystems a lot (but also Zig and well done C libraries) and I'm trying to learn a tiny bit by their success and bring it to C++, that is my favorite language)

No problem at all for your initial comment! I share similar sentiment, I've found a lot easier in the past glueing C libraries to do something more than trying to integrate a C++ library for the exact reasons you're describing...




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

Search: