4,223 karma · joined May 21, 2015
There are so many other interesting and relevant invariants that are usually in an API contract that are much harder or impossible to check upfront (let alone express formally in a type system), and even violations may be impossible to diagnose when they happen.
People focus on NULL because that's the only way they can apply their silly limited type systems. But NULL checks give very little return for investment. In practice, you'll see templated Option<T> types and whatnot, and when I have to look at or even work with such code I want to kill myself because it's so painful.
We are talking about an extremely simple straightforward API with an obvious contract. It's good enough for this function to reliably surface almost all wrong uses with a segfault immediately. Wrong use will result in segfaults and otherwise bugs and crashes. The goal is not to work when used wrong but to work when used right. You cannot save the world from scratch in every little function. You still have a job to get done, and you have to move on.
But it's >> 99.9% true that this will just crash even though it's acshually UB, nasal demons and so forth. Now raise this << 0.1% likelihood that it isn't true on some system with some compiler and build flags, to the power of the number of distinct deployed configurations out there, and you get the result which is the correct engineering decision of just moving on instead of spending your life filling straightforward code with pointless boilerplate assertions.
NB it can make sense to assert nonnull when the condition won't be tested on all code paths or the intention is otherwise not super obvious.
I just had a look at your HN profile page and was struck by the irony of seeing your Forth vs Lisp vs Postscript code examples there. Now consider that I've never written code like 4["Foo!"], even though I know it's possible, but in other languages you constantly have to do mental gymnastics to get any real work done, and those are allegedly so much saner !???
That easily gives O(1) access to enqueue/dequeue, and the queue naturally grows and shrinks (even within configurable bounds) as part of reading and writing. Blocks can be taken and returned from/to some fixed size block pool. This design would be completely satisfactory. It's also very easy to code from scratch, so why should I settle for anything less, for example a growable vector where a reallocation would mean blocking other threads for extended time? But I was told to "not reinvent the wheel" and to "not overengineer it" so I tried to change my mind and make use of an STL container data structure.
> our disagreement seems to be that you think a slightly different terrible data structure would be a good idea
I don't know why you think that I would be thinking that, and I'm confused that you say you were gesturing at how std::deque is terrible, and not sure why I should be the one misconstruing something, when my premise from the beginning was "STL containers are terrible in many ways". And I don't get at all why you responded with "basically std::deque" to my "maybe chunk lists would better". It doesn't make the least bit of sense to me.
But let's settle this here, this is a pointless fight...
Without knowing more about the details of the spec and of real world implementations [0], I'll boldly claim that the O(1) is the precise problem why this is a weird data structure (I never needed or expected that sort of constraint). It is not a chunk list, like many (including me) seem to have assumed.
I don't even have performance problems measured with this, also given that my std::deque is not currently in production, it is used as a simple streaming queue that only has a couple megabytes/second of bandwidth requirements, and it is not being built with MSVC (so chunks are going to be 512 or 4096 bytes). But it is a latent problem.
[0] Btw. I don't want to know the messy details of this right now, I need simple not complex. I've learned not to add more complexity in a futile attempt to fix things that are fundamentally broken. What use case is this going to be solved by std::deque, name me one such use case and tell me with a straight face that it's not a completely made up case or a super niche case that would likely also be better served by a different approach.
Or take it from someone else if you would, is Chandler Carruth high-profile enough and free enough of conflict of interest to be credible for saying that std::deque is dumb? https://news.ycombinator.com/item?id=22962980
But ACSHUALLY, how you write allocation is like this
#define sane_alloc(type, count) ((type *) malloc(sizeof (type) * (count)))
game->boardPieces = sane_alloc(BoardPiece, row * column);
The kernel people seem to finally have figured out this one in 2026.As I said, I don't think growable array data structure is a good idea.
> Basically std::deque. Surely no more need be said ?
Ok now you're really out of your depth here. Pretty sure you haven't actually used it, it's a somewhat obscure data structure even though the more serious C++ programmers have heard of it, most have probably never used it.
From a web search, std::deque seems to be pretty universally thought of as a weird and very bad data structure. I've had to learn myself because I did actually try to use it myself recently. Beyond being unintuitive to use due to being "abstract" (had 2-3 serious bugs, e.g. unexpected iterator invalidations that happened only under load), apparently std::deque is not even specified as a chunk list. O(1) random access must be guaranteed. So it must be much more complicated than that, I don't know the details though.
And while actual implementations do use chunks in some way (just not in a simple chunk list), apparently "chunks" is not a part of the std::deque spec, and hence there isn't anything standardized about the size of these chunks either.
- On MSVC, chunk size is 16 elements so chunk size of std::deque char is 16 BYTES !!!!! I was thinking they can't be serious, but apprently it is the case
- On GCC, chunk size is 512 byte fixed.
- On Clang, chunk size is 4096 byte fixed.
It _is_ a shithole, surely no more need be said?
If you're counting pennies, Vec::reserve() (inexact) is hardly what you want, because in the worst case you're wasting a factor of 1.5x or 2x of elements due up-front overallocation. Maybe chunk lists could be better, overhead is bounded by chunk size and all operations are constant-time. No pointer invalidation either. And you can pool those chunks, preventing memory fragmentation and improving memory utilization, since there aren't a million different sized allocations in your process.
Google also recommends using Golang in many cases, which was explicitly designed for not-so-experienced people. It is more geared towards creating services quickly and robustly, not towards squeezing out the last 10x of performance.
I can't say anything about abseil, having never used it. "By Google" is not an immediate seal of quality, and it doesn't mean that one size fits all. I've recently seen a list of .so objects required in order link grpc (also by Google), there were like dozens of abseil .so's in the list IIRC. I don't like it! In my experience, validated countless times in my own practice, complexity => slow and hard to maintain and simplicity => fast and easy to change.
Templatized C++ containers are generic recipes, they can't make use of local context and hence don't lead to the simplest solutions. They produce tons of boilerplate. They give you a decent single-threaded baseline performance in many cases, sure, but now start hammering these data structures using 16 CPUs in parallel... you might find that you should have designed your application completely differently.
Like WalterBright says, I too use very simple data structures almost exclusively (arrays, linked lists, linear allocation). Most of these coded ad-hoc everywhere, very straightforward, very adaptable. Even hash maps I only use infrequently -- if I can, I just make keys such that I can index directly. I get performance from knowing exactly what happens, and minimize the work that needs to happen. I create immutable data objects (write-once) where possible. I've created my own pooled block-allocator for power of 2 sized allocations with book-keeping in shadow-memory (preventing fragmentation), massively reducing syscalls that modify virtual memory mappings and closely controlling memory used by various subsystems.
I don't expect a library like abseil to solve many of my problems, it probably makes some things "easy" by taking away the very control from me that I need to solve the problem I have, resulting in unsolved problems and complexity.
Yes, Rust version allows you to maybe skip a reallocation step or two by doing explicit up front reallocation. But remember most allocation work is always from the last grow anyway. The Rust version seems like a microoptimization, giving a little bit more explicit control in a situation where you've already pretty much given up control and gone like, throw hands in the air, we're doing push_back()!
Realloc is pretty much never the right way in whatever form, and I've never seen any need to include realloc in any of my own allocators (mostly blockallocators and linear allocators and pools/free lists, sometimes using malloc/free).
A name like "Rectangle" will only have collisions if there is a C file that simultaneously includes headers from two distinct libraries that define that name, which frankly is very unlikely and very easy to work around. As long as you don't define a silly constructor that creates a symbol Rectangle::Rectangle(float, float, float, float) or whatever, there will not even be linker collisions when distinct parts of a software include distinct definitions of the Rectangle name.
Nevertheless, I simply name my types like "VxRect" or whatever in case they are supposed to be consumed by other components. Much better than Vx::Rect or even Company::Math::Types::Rect, isn't it?
But already the basic premise that you should push back without thinking is wrong. You will suffer reallocations and invalisations when you least expected them, and frankly you have to architect around that fact which is a terrible restriction. You can work around by pre reserving but at that point it's just a basic fixed heap allocated array but worse because the type gives you a weird look all the time, "I'll realloc as soon as you don't pay attention, harhar"!
And I'm working on this project because the material removal simulation library that my company has been paying insane amounts of money for in the last 10 years just doesn't "cut" it for our grinding purposes (independently of the cruelfully bad C++ API, and the ~10x worse performance in their debug build because of all those dumb C++ objects inside), and it requires an insane amount of BS architecture around on our part to even use it, so I've concluded (after my own short periods of serious well-meaning suffering with this library on and off during the last 2 years), it should actually be way easier to make our own grinding simulation in a short time after running the numbers and concluding a realistic magnitude.
Guess what, I still see no reason why my little project shouldn't succeed, I've seriously come closer to realizing it, but COM Objects by Microsoft are once again not the reason why that is so. On the contrary, they are annoying to use (I'm not a newcomer to them), even though there are a lot of nice things to say about Microsoft APIs as well (and OpenGL should just die), it seems like a layer of bullshit to wade through for no obvious reason.
The nicest libraries in terms of usability have always been those with a simple & straightforward plain C interface.
(Obviously I meant to say low latency, not low frequency)
They're also unusable by the way because of ergonomic and software architecture factors, such as bad modularity, terrible compile times, unreadable error messages, unreadable symbol names...
Yes that is overgeneralizing a little bit but it's largely true.
The problem is typically not the containers themselves but all the other bad decisions that they push you to make in order to work around their "small issues".
The huge problem is that these containers can get you started quickly, i.e. leetcode type stuff and single threaded stuff, but at some point you'll realize your architecture ended up completely in the wrong place because of that.
If you haven't been thinking deeply about memory management and concurrency, you won't be able to understand, no offense meant. I've just fixed another subsystem that was completely overwhelmed, seeing 8x bandwidth gains already on a small testsystem, but the factor is basically unbounded when moving to bigger systems, when it's about contended vs uncontended.
And namespaces suck too, so much noise for little gain. You know what, a big part of programming is naming. You just have to come up with good names. Namespaces don't magically make names better, if anything, they make them worse. And they add a lot of syntax noise.