Sadly, this is representative of the new trend of C++ programmers.
I will get down voted, hated, and laughed at, but I do prefer the original version of the code.
It is infinitely more simple and readable, and the layers of abstraction added are of pretty much no value here.
If I stumbled across the latter version, I would just scratch my head. And I've seen sooo many projects just end up in a big code bloat just because some programmers wanted to add tons of "new" features like this that add nothing and just complexify the code for no reason.
The second version is quite convoluted but it makes sense. But as you say, in this instance I don't think it is a problem that really needs to be fixed when the first one is quite obvious and simple to read.
And to me, the first version does look like someone used to C who comes to write C++ but writes their C++ like it is C. Wrapping it in the second version (as others have pointed out) is safer because it is reusable, and cleans itself up.
I don't understand how you can say that with a straight face. The ArrayView class is incredibly simple and takes about 10 seconds to understand. The "complex" stuff is in the make_contiguous code, which in the end is simply adding integers taking into account alignment requirements (which you still need to understand if you want to write the first version in the first place).
And seriously, did you read the initial point? The problem is that the first approach must be fully rewritten for every new class with different members that exists in your code. Given how HN is butthurt over off-by-one and out-of-buffer errors, you can't say that the first example can really be debugged easily when you have 30 of them in your code. If you only had the Mesh class in your code and nothing else similar maybe it would not be worth it.. but only maybe.
The final example is incredibly simple to use, hides the complexity in one single point and prevents you from ever making mistakes outside of it and seriously, it is not that complex.
Sorry, but again I have to disagree with you on every point. I may appear as an old grumpy programmer, but this kind of article exactly falls in what Zed Shaw calls "the expert", and I totally agree with him. (http://zedshaw.com/archive/the-master-the-expert-the-program...)
There are two 3 wrong premises in this article (IMHO):
- It makes you think that using raw pointers (so called 'low level types' in the article) is more error prone than using combined types.
- It makes you think that adding abstraction removes complexity.
- Makes you think that you should combine/factor everything because repeating yourself is forbidden.
These assertions are straight false.
- Adding abstraction does NOT remove complexity. It ADDS complexity, and then HIDES it. The ArrayView class has absolutely no use whatsoever. Every C/C++ programmer is used to access arrays through pointers. Creating a class for that just makes a reader wonder "what the hell is this thing?". And then you have to crawl in the code to understand what is this thing. And of course, in 3/5 years from now, this ArrayView class will have tripled in size. For something as simple as reading numbers in memory, i don't want any abstraction.
- There is nothing wrong in repeating yourself once on a while => if it improves readability! Factoring too much IS wrong. It makes reading the code a pain.
When I read code, I don't want to read an abstract version of what the code does. I want to grasp the maximum possible information with the minimum noise.
After reading the final version of this Mesh class, I have no idea of its memory layout. I see some ArrayView and a magical create_contiguous_memory function. I'm lost. I don't know where the memory come from, whether or not I own it, what will happen if I hand a pointer to it. Everything is hidden, and that's a pure pain to understand.
It seems to me that you simply don't like what C++ stands for, and that's perfectly OK; but then complaining that C++ follows C++ principles seems foolish to me. To quote the original article:
> should we be using these low level types in this particular class, or use classes that better represent what we need and leave the low level types to them?
There is no mention that we want to remove complexity. Instead, the aim of modern C++ is in fact to hide it, so that high level code can be written as simply as possible while hiding all the implementation details in classes. The code you see in this article is no different from the code you find in the implementation of the standard library; in fact since it is simpler it is even easier. Does not mean that the standard library does not have complexity - but it hides it from me, possibly with low expense. Also it never mentions pointers, the problem there is all the arithmetic which is not portable and highly prone to bugs should it ever need to be modified.
The article is not factoring. It is coalescing a feature in a simple, single purpose class which does very little, and has a very simple use case and usage. If you have no idea of the memory layout, then you need to read the class, exactly as you would if you wanted to know the memory layout of std::list or std::vector. Making a class does not prevent the complexity from existing, it just prevents you making silly mistakes while using it. It never allows you not to know your trade and forget how to program.
It seems to me you are a C guy. Really, I get what you are saying, it is just not the philosophy of C++, and if you disagree, maybe you shouldn't be using C++. That's perfectly fine, and I love that there are C people doing C things - it's not my cup of cake, but I'm happy they're there. And I would understand also if you were complaining about C++ trying to do something it is not. But what's happening here it really not the case, so I don't agree with you either.
It seems to me that you have a particular idea about what C++ "stands for". C++ doesn't stand for ANYTHING. It's a design-by-committee warty mess that was ill-born to begin with and the surgeons have been grafting new limbs onto it for years trying to make it less horrific. It may now resemble Cthulhu, but pretty it ain't.
In point of fact, I agree with what he said, it accomplishes in me an understanding that in fact C++ is a warty thorny bush of mess, and leads me to conclude that a significant aspect of 'being a C++ coder' is to find ones way, beautifully or otherwise, through it all.
I've seen great C++ code that I just thoroughly enjoy reading, because it doesn't make assumptions about anyones' intelligence, and I've seen C++ code that is purely designed exclusively for only the elite who are mighty enough to wield the sticks that the C++ committee provide. C++ really gives all kinds of control authority to any style you wish to use .. another of those quaint software ironies that the power that makes one weak, enlightens another ..
C++ is getting better, but its early development was driven by questionable features (iostreams, ugh) and denial that the language / runtime needed some basic features, like string and buffer types that didn't suck.
I've been using the language since 1987, and still have my own safe subset, augmented recently with some of the C11 stuff that's become solid in the toolchains I use.
But it's still a misfeature-ridden grabfest of a language.
I was hoping to share an opinion. I don't hate C++ when I use it - like every other C++ coder with their sanity intact, I use a subset that I understand and which usually doesn't blow off my toes. The problem is that invariably your safe subset intersects with someone else's and that's when the pain starts.
Every C++ programmer I know uses their own narrow subset of C++. Except that those using "hardcore" ++ features appear to be far more vocal about their subset being the one true way to use C++.
You're sort of right. I don't like what C++ has become.
I liked C++03 and hoped C++11 would just have added fixed size types and C99. Nowadays I mainly code in C.
Still, this article is the exact reason why I just can't stand "the new C++ trend": Code bloat for no added value, false sense of safety with fragile abstractions, pointless factoring which makes code unreadable :(
How many errors are there in the original "simple" code? I mean dangerous and non-obvious logical ones, not the trivial syntax errors. The ones I can see are:
First, the computation of uvs_size is wrong; it's using sizeof(indices[0]) rather than sizeof(uvs[0]). Since the the former is an int and the latter is a Vector2 (presumably 16 bytes), the system doesn't allocate enough memory and there will be a heap overflow.
Second, it tries to do pointer arithmetic on void pointers which is undefined.
Third, it doesn't take into account alignment requirements. For example the Vector2 class would ideally be defined as 16-byte aligned so that the Vector2's can be operated on by SSE instructions. This code gets that wrong or right depending on the number of integers in the adjacent array. So in the worst case it'll be lovely code that works until a data file is changed, and then suddenly starts crashing in odd places.
Sorry, screw raw pointer arithmetic when 7 lines of code have three non-obvious errors, one of which is extremely serious. It seems like a pretty good counterargument to your claim that using raw pointers is not error prone.
The number of errors there is frightening. I only noticed the use of void-pointer arithmetic and lack of alignment processing.
I would say the code doesn't go far enough. Instead of using `std::unique_ptr<char[]>`, it should define some kind of memory_block abstraction (using `unique_ptr` internally), where the copy constructor and `operator=` both do a byte-wise copy of the data. Then alongside `make_contiguous`, you would have an analogous `copy_contiguous`, where the base case of `make_contiguous_helper` would instead call `memory_block.resize(total_size)` and the base case of `copy_contiguous_helper` would call memory_block = other_memory_block. This would remove the boiler-plate code for the copy constructor in Mesh, which can instead be:
I considered that. The problem is that memory_block would need an extra pointer to know its size, which seemed like pure waste at that point. And ultimately it doesn't relieve you of writing copy constructors etc which should be the real goal; it scales much better if you have a class with other members that are not a part of all this contiguous stuff. I hope to solve that in the next post. I think your approach is good as well, just giving you my reasoning for not putting it in.
Rather than two pointers I would probably have pointer + len. Storing the length of your arbitrary memory block is no bad thing, imo.
In any case, having a sensible copy constructor on the memory_block (and any necessary associated data) would at least make the copy constructors in the client classes (assuming you have more than one user of this pattern) cleaner.
It's a really good pattern, though. Excellent blog post.
Guys, there are errors in this code because I just typed it into a text buffer without ever compiling or running it. It is an example from a slide that I didn't care very much about, for illustration purposes only.
If you did that with the C++ code in this article there would be errors too. Major straw man.
Wasn't your whole point with this language feature that the pointer manipulation code is really awkward to write, and so hard to read that you're never quite sure whether it works or not? :-)
The bug where you copy a line, need to change a bunch of spots and miss one is a common pattern. It happens even when writing real code into a real editor/IDE, and especially when dealing with code that has lots of repetition (as advocated by Galanwe). It's also usually not detected by the compiler, and detected at runtime by simple smoke tests only if you're lucky.
So I don't think it's a strawman argument. It's exactly what you'd expect to happen at some point when using a style that encourages copy-paste programming. You can easily find lots of examples of similar bugs in the wild, in production code that has been running for months or years. It's just fortunate that the example code had an instance of that bug pattern.
But also, even if I didn't have a choice and had to write the code in C++, I would do it in the style on the slide, not the style endorsed by the link on this HN thread.
I do agree that copying a line and changing spots is a common mistake pattern. However, I also still think that in many cases that is the best thing to do because it results in the simplest code. So rather than go through contortions in the actual code to try and prevent this, I am wondering if some kind of IDE pattern matching is a better way to catch this class of errors.
FWIW, I assumed as much when I wrote the post. I wrote merely that the code is bug-prone generally, without pointing out specific bugs. A stance that I got the impression you agreed with, from watching your video.
In generally I tried to avoid an overly negative tone when referencing your code or statements, I hope you find that it's reflected in the post.
I think that you are misattributing premises to me.
I think that a "combined type", can be safer if well written than a raw pointer. Surely you don't disagree with that? That's not the same as saying all combined types are better.
Similarly, adding abstraction can be good, it can also be bad. C adds abstraction to assembly. I'm guessing though that you would choose C over assembly for many things?
Repeating yourself is a bad thing in general. It could be that to get rid of the repetition we'd have to incur other costs that might be worse. I don't think that's the case here.
I just don't understand your "magical create_contiguous_memory" comment. Do you feel like the sort function is magic, and instead we should write all of our sorts out at the call site? More practically, we write a good sort once, document it carefully. The users of sort know roughly how it works and exactly how to use it. In the rare cases they care, they read the code. make_contiguous is exactly in this boat: you write it once, you write it carefully, and then you use it without worrying about the details every single time. Our brains are just too small to deal with all the details all the time, that's why we're trying to hide complexity that isn't as immediately relevant.
Your post is mostly sweeping generalities which aren't true in general, and as far as talking about the code goes, you mostly just say you don't like it or find it hard to read, and brush aside anything concrete like reusability, testability, bounds checking, etc, specific things that are actually useful.
From an ok, not great programmer with a year's experience, I wouldn't necessarily expect them to fully understand make_contiguous right away. But I would expect them to understand who owns the memory, and the memory layout.
It's simpler at the expense of being more bug prone. Notice that the "simpler" struct has no housekeeping code to "delete[]" the memory. The programmer now has to manually "manage" that cleanup somewhere.
>, and the layers of abstraction added are of pretty much no value here.
The "value" he's addressing from the "simpler" but more fragile code:
1) remove bug-prone code which requires manually synchronization (copy&paste) of pointer types in both the lines defining the variable and the subsequent lines casting the pointer from a raw buffer: "mesh->positions = (Vector3 )mesh->memory_block;"
Calculating offsets (and/or enforcing logical boundaries) inside of arrays is very error prone and the OpenSSL Heartbleed bug was an example of that type of defect.
3) manage the release of memory using unique_ptr. Obviously, it is to help the programmer (and other coworkers using Mesh data structs) avoid memory leaks.
>features like this that add nothing and just complexify the code for no reason.
It doesn't look like "no reason" to me. The attempt was to reduce bugs, and add memory safety.
I agree with you. While I don't code c++ now it used to be my tool of choice (right back to when it was a c pre-processor) and every time I look in, the language is uglier and conceptually heavier. If there's two ways to solve a problem, c++ will provide three - none of which work on their own without gotchas, none which play nicely with each other and each which will be required for working with some 3rd party library you need.
Quite. It's been going on from the outset. Basically they got it wrong and they've been hacking at it ever since in a vain attempt to get it right. The should have killed the C linking compatibility from the outset (header files are devil spawn), they should have had a native string type as well.
D represents a much better attempt at what C++ ought to have been.
I could not agree more with you. You would never write a class like that for a skinning system, it's full of bloat and template guff.
Also at some point you're going to have to promote this to the GPU, at that point the original code will have a closer affinity to the GPU code rather the nonsense c++. I can forgive this though as he states he's not a games programmer.
The original version is terrible. C code written in C++. Basically an high level assembly language. Write all your code this way and soon you'll stumble with all the nastiness of manual memory management.
Code like that is the main culprit of C++ is still considered an "unsafe" language, despite a correct usage of it, although more verbose and with more abstractions, brings both the benefits of C-like speed and higher level languages safety.
>Write all your code this way and soon you'll stumble with all the nastiness of manual memory management.
See, this is what I never quite understand. I use C++ when I want to do C type things. Manage my own memory. Bit twiddle with speed. Avoid all dynamic allocations. Increase cache coherency. Call inline assembler. Use the CRTP to achieve static polymorphism. Design my own containers. Lock free containers with atomics. Etc. If I'm using C++ I want nastiness. I want to get down and dirty.
Why on earth would you ever want to use it as a higher level lanuage? It sucks balls at that. Just go and use a proper, nice, well designed high level language instead. If I want a language that holds my hand and protects me from the evil pointers, I'm not choosing C++. Using C# or Java (for example) is complete luxury compared to C++.
But apparently, there are a whole bunch of people that do attempt to use C++ for this purpose. Who strive to achieve a language where you never see a raw pointer, but you are still stuck with maybe 30% of the productivity you'd have in a proper higher level language. It boggles my mind. Congratulations, now you have no garbage collection, a crap IDE, you're still wrestling with forward declarations of classes and header files and the preprocessor and linker options and slow compilation and you've now buried the machine under a layer of abstraction. The absolute worst of both worlds.
>brings both the benefits of C-like speed and higher level languages safety.
Except that it does. This article clearly demonstrates one way that C++ provides higher level safety without overheads - the allocated memory is guaranteed to be freed.
Nobody is saying you can't do low-level stuff in C++. They're just saying that C++ provides a zero-overhead way to do it safely. Why wouldn't you want that?
Except many provided abstractions are not really zero-overhead and it is even debatable if they are lower-overhead than abstractions found in the higher level languages (like mentioned here Java or C#).
Use std::string everywhere and the (re)allocation rate / fragmentation will make your allocator very sad. C-style strings are not "fast" per-se, but at least they make all the overhead explicit.
Use std::shared_ptr for everything and you'll end up with a higher overhead than any proper GC, particularly on a multicore system.
Use templates too much and your code size will blow away any L1 cache very soon.
Use std::mutex/std::lock to protect shared state and it is going to cost you much more than synchronized in Java, which Java often turns into a noop, but C++ compiler can't.
Use virtual functions and (in all except very trivial cases) they will prevent strong opimizations like inlining in many cases where C# or Java would have no problem.
Want to have safe array access? Sure, still possible to add range checks (e.g. use at instead of []), but are you really sure C++ compilers are as strong in eliding these checks as compilers for the languages with mandatory array bound checking?
Oh, you can even go the Java/C# style very heavily and use Boehm GC. Because you can. Who said C++ is lower-level? Except it is nowhere near the performance levels achieved by proper modern GCs.
Sure thing, C++ is now very slow. That's why big projects like LLVM/Clang, WebKit, Gecko, Internet Explorer, AutoCAD, all Adobe products, all AAA 3D games and most indie ones use C++ instead of Java. If they knew back then when they started that Java and C# have lower overhead than C++!
Funny you listed mostly desktop Windows applications, where C / C++ API were just the only choice in the times when these applications were born. All of the three (Java, C# and C++) are used in performance-critical applications these days, but you just need look at computing as a whole, and not just at a tiny part of the market (desktop).
Also, not sure why if someone points to the obvious places where C++ could be slow, people treat it personally as an attack on "their" language. The fact that many high-level abstractions are not-free in C++, doesn't preclude it from being still highly performing, particularly if you know the cost of these abstractions and you know when to avoid them.
The point isn't necessarily about whether C++'s abstractions are faster than Java's, the point is more that you aren't always forced to use them. If you are willing to commit to the size of your string at compile time, you have the option to use char[N] instead, avoiding heap allocations. Or you can use a stack-based allocator for std::string.
The thing about templates is misleading. Sure, if misused they can hurt your cache. But they also move branching from run time to compile time, which is a very good thing. Google some benchmarks of C++ sort vs C qsort; the former is much faster because the comparator can easily be inlined.
The templates used in my post, for example, either bloat the code not at all, or very little. They are either generating tiny functions that get inlined anyway (like ArrayView; once a function is inlined its irrelevant whether it came from a template or not for code bloat purposes) or they are generating code that would just need to be written by hand (like make_contiguous).
I've actually personally witnessed two fairly detailed accounts of people that actually got themselves into situations where template or template-like bloat became a performance negative, relative to the benefits they provided. They didn't cite a cliche about code bloat, they actually benchmarked and found the cost. What both of them were doing was very extreme; nobody who's not using very extreme template techniques (and therefore is sold on it) is hitting that point.
The current trend is for C++11 libraries to be header-only, lightweight and specifically without the hacks that have traditionally been required to work around all sorts of compiler issues.
The original version is not only bug prone. In some scenarios, both versions have a potential vulnerability. Can you spot it?
In a scenario where users can access elements of a joint array of requested size without limitations, they can access memory outside the array. Memory block's total requested size may overflow size_t. Fortunately, overflow prevention is easy to add to the generic version.
> both versions have a potential vulnerability. Can you spot it?
What the hell are you talking about? It's a game.
> In a scenario where users can access elements of a joint array of requested size without limitations, they can access memory outside the array
Welcome to modern computing! A process can access any part of its mapped address space without limitation.
Joke aside, are you suggesting a process should hide memory from itself?
Forget it, I can't even make sense out of your comment.
> Memory block's total requested size may overflow size_t
Are you serious? Then please suggest a fix for C, C++, Java, and whatsnot, because nothing will prevent you in pretty much any language to ask for more memory than you can handle. This is not a _bug_, it's just expecting the programmer not to fuck around.
Example case of where it matters would be modding. There's an expectation that installing things like new textures and new models is safe. Likewise for making changes using a game's internal scripting system. If these external resources aren't properly validated, they allow a apparently safe mod to execute arbitrary code. Doesn't matter that it's a game, it's bad either way.
I'm not talking about game rendering, but multiplayer games and game servers might be affected. I mean, the requested size must come from an external source.
>(...) could allow an attacker to execute arbitrary code with the privileges of the current user.
>The vulnerability occurs when reading the header of the Pack200 compressed Jar file during decompression. To calculate the size of a heap buffer, the code multiplies and adds several 32-bit integers. The bounds of these values are not checked, and the arithmetic operations can overflow. This results in an undersized buffer being allocated, which leads to a heap-based buffer overflow.
I remember there was a similar bug in some other decompression library.
There is a third way... don't write ArrayView<> and put all the "C style" code in the constructor and destructor of a Mesh class. Then you get something you find intuitive and you get exception safety.
Your comment that this is less readable is valid. Readability varies widely with background, skillset, consistency, and a host of other factors. If your team finds C-style code more intuitive, don't write Java style code. If your team finds malloc error prone, use smart pointers.
Perhaps the biggest advantage of C++ is its flexibility, at least compared to the other mainstream languages. This means everyone can write code the way they want and others can use it (a).
(a) OK. There is still a large list of gotchas around types, the preprocessor, header inclusions, namespacing, etc. that you need to know about to be a good citizen, at least until C++ can figure out how to support modules.
Post author here. Sorry you feel that way. I'm surprised you don't think that moving out the messy code into make_contiguous is a win. Isn't it good to factor out clearly reusable code into functions, so you can reuse it? Would you really prefer writing out code like that in a dozen places for a dozen classes that needed contiguous storage, to writing one function, testing it, and then calling it from all those other places?
The 'functional people' are currently anti-reusable code. It gets in their way and complexifies their function graph. Which are bad things for them.
As a lifetime C++ guy I find this new paradigm (inline code a-la structured code) brittle and fragile. App assumptions are laced throughout; nothing can be changed without a rewrite. And my go-to code-reading techniques (follow a class through all its incarnations with grep etc) fail - they rarely subclass, just overriding existing classes locally to create callbacks etc.
So I'm not a fan of the new order. I see it's a productivity tool for the app writers out there. Especially with the one-off code that startups write, expecting to do a rewrite once they're funded. It really makes sense for them.
Hi, Another Game dev here.
Firstly, neat article, thanks for writing it.
I'm curious as to the problem you're exploring/solving here?
"Concatenated struct's in single chunk" is a neat trick. The use case seems a bit broken[1], but I'm assuming that it's from the original video, and your c++-ifying it? Or at least trying to find a nicer way to write this style of code?
If you'r going this route, I'd be tempted not to use pointers at all, and just use offsets and accessors.[2]
* Lack of pointers means you can load in 1 read. (endian needs to be watched, and assumes you can get the entire size elsewhere)
* You can also shuffle it around in memory should you be doing something fancy. (defragging heaps might be useful if doing a sandbox)
* Better encapsulation? maybe..
But the basic point I'd like to make is, there is no nice way to write this type of code, because c and c++ gives us no nice[3] way of expressing it.
[1] Verts and Indices are generally uploaded to a GPU then discarded, but this has the counts, which is needed CPU side for draw calls, in the same chunk so would need to be copied before freeing.
[2]
struct Mesh
{
int32_t num_indices,
num_verts;
// inline to this struct..
// assumes Vector3 is aligned 4 bytes.
// and that Mesh has been alloced to same alignment.
//
// Vector3 positions[num_verts];
// int32_t indices[num_indices];
// Vector2 uvs[num_verts];
inline const Vector3* PositionsBegin() const { return reinterpret_cast<const Vector3*>(this + 1); }
inline const Vector3* PositionsEnd() const { return PositionsBegin() + num_verts; }
inline const int32_t* IndicesBegin() const { return reinterpret_cast<const int32_t*>(PositionsEnd()); }
inline const int32_t* IndicesEnd() const { return IndicesBegin() + num_indices; }
// etc
};
[3] Maintainable, fast at runtime, low mental friction to people other than the author, etc
Hi, thanks yassim. Yes, the use case is from the original article. Yes, that is certainly another way to go, you could just provide the unique_ptr and a bunch of integer offsets. In this post, my data structure is trying to balance performance and code cleanliness, it's advantageous to provide standalone ArrayView which can't just be two integers. In the follow up post I'll be trying to make the data structure itself generic, so it will more easily allow for space optimizations.
I guess we'll agree to disagree, I think that C++ gives us some nice ways of writing it, I think that's what the post shows :-)
There are prebuilt slab allocators that do this well. I used the arena allocator in facebook's libfolly recently as the base for a huge page allocator, and the code turned out quite nice. I think the allocator is a much better abstraction here. You could use something like the arena allocator here to carve out the memory for all the vertex and mesh storage and amortize the cost of hundreds of objects as well, so I don't like the argument in the first few sentences of the article actually that allocators would take more space.
Exactly. Stateful allocators were added to the standard library especially for this sort of problem. Before, allocators were assumed to have no internal state, which made it problematic to manage memory from an arena or pool.
Perhaps there's a reason allocators wouldn't work here, but if so, it deserves a little discussion.
I did discuss stateful allocators. They're nice, I'm a big fan. But as I said, they would impose pointless space overhead of one extra pointer per array. Also, you now have more complicated issues: each of the arrays is now an owner. What if you try to make a copy of it? Move construction? What if the container tries to deallocate, how does our allocator handle that? When I copy Mesh, would I need to change the allocator's state in the copies? How would that even work? I think that once you think about trying to accomplish the very specific and relatively simple thing being done here with allocators, you'll see that it would be quite a bit more complexity to no benefit.
I think I did give it a "little" discussion :-). Guess it depends on your definition of a little. I didn't want to talk more about it because I didn't want to get sidetracked, and this is ultimately the way I chose to do it.
Hope that sheds some light on the post and the choices I made with it.
The whole point is that they shouldn't, because there are multiple loops over the different attributes and for best performance they should be in individual contiguous memory regions. Remember that you only have a few milliseconds per frame in a game and those things can matter here.
The two attributes are pos and uv, which are almost always tied to each other (and therefore be updated together). The GPU will access attributes by index hence they should be interleaved otherwise you are just forcing cache misses.
Indices are not vertex attributes and thus should not be interleaved with the other data.
I watched the linked original video to understand the motivation, and read the article, and I think this is a misguided solution to a real problem.
These "joint allocations" seem to be "a small pool of heterogenous types." The motivation is that heap allocations are expensive, so we want to coalesce them. The only way this would matter at all is if we are creating lots of these Mesh objects.
The given solutions make one heap allocation per Mesh object, as opposed to three. Create six million Meshes and you get two million allocations.
Instead, one pool should be created for each of the three array types: Vector2, int, and Vector3. Now, create six million Meshes and you can do somewhere between three and a few dozen allocations, depending on your pool's initial size and growth strategy.
But if you only ever create a single Mesh, then you wouldn't care to coalesce the allocations, because three allocations instead of one is inconsequential in the course of an entire program lifetime. I agree that there are many vertices and edges within each Mesh, but there better also be tons of Meshes within one program, or there is no problem to begin with.
I believe that the main problem is not allocation time (games already have loading times, that's not a problem), but fast access and not breaking the cache.
The title of the first slide in the video is "Heap allocations are expensive!" And using a pool the way I'm proposing does not fragment the heap. It might be slightly worse if you serially touch a lot of Meshes which have only a few vertices and edges each, but this is a corner case and a trade-off anyway.
Meshes are typically quite big. Moving two small blocks of data together helps to improve cache behaviour as the two blocks can then fit in a single cache line. Moving two giant blocks of data closer together is not going to make much difference, because they probably span hundreds or thousands of cache lines anyways.
I don't know as I'm not a game developer, but my guess is that individual Meshes may correspond to things that need to be created and destructed on a regular basis during runtime. If that's the case, then depending on the details it may be preferable to have an array of Meshes, and not try to pool all the Meshes together.
I think it's risky to say that it's misguided without the full context of the problem.
If you ever start from something like this, you should be asking a lot of more-basic questions first, such as:
1. Why is the goal to put everything in one block with a single allocation? Could everything still work if they were separated?
2. What do Vector2 and Vector3 look like? What if they say "int a, b" and "int a, b, c" respectively? If a perfectly-aligned "new int[total]" would have fixed the problem, it should have been tried from the start and the code refactored accordingly to not necessarily use structure types to look up the data.
3. Conversely, are Vector2 and Vector3 complex classes with special constructors (or equally important, could they someday become that way)? If so, even a "fixed" memory-allocation solution will be equally fragile to maintain because there is a responsibility to ensure that constructors are called correctly.
4. What is the memory profile of the rest of the application, e.g. how many Mesh objects themselves are created and is the entire approach to managing Mesh objects wrong? Maybe the focus on optimizing one piece has missed an entire problem somewhere else that is more fundamental.
Clearly the original code has bugs but it is also only 14 lines, the fixes for the bugs are straightforward and the right solution (after other analysis) may well have been to remove the code entirely instead of doing the same thing in a different way. Beware of the tendency to "fix" things without looking more deeply at the actual problem.
I was recently bit using a similar technique to the original code for an intrusive type in C. Using manual non-typesafe raw offsets for things can definitely lead to nasty bugs.
Automating the calculation of offsets and assigning pointers definitely eliminates a lot of potential bugs, but I do wonder if this isn't a bigger deficiency in C++. Why force storing an extra pointer per array? I am still not sure why C++ doesn't allow FAMs [1].
There's probably a question of which would be more efficient, storing pointers in the object and calculating the offset from the pointer, or dynamically calculating the offset within the object. Still, it doesn't seem like a problem developers should need to solve.
To allow FAMs for non-trivially constructible/destructible types would be slightly more complicated than C FAMs, but it doesn't seem like an unreasonable thing to expect from the language/library, or intractable from a standard or compiler perspective.
Regardless, I'm still not sure what the justification is requiring it be UB even for just primitive types.
It's not really an extra pointer, unless you assume there are no alignment issues. If you assume that there's zero padding between members, then yes, you can do 1 pointer for storage + 1 per view. In the follow-up post, I plan to either do one pointer per view, or one integer per view, haven't decided which.
The thing is, who should solve it? The different approaches you listed have different advantages, there isn't one right answer. If the language itself solves it for you, you are stuck with whatever solution the language picked. This would be fine in a higher level language, but not in C++.
You're right though that individual devs shouldn't be solving it, it should be in a library. If there's enough interest in these posts, I'm happy to put up my work (fully fleshed out and documented) on a github for people to use.
I suspect the compiler will be better able to optimize offsets than pointers just because of the semantics required by the language, but I think you're right that it isn't necessarily one size fits all. Another possible solution might even be storing static bounds at specific intervals. Without numbers, I can only guess which would give optimal performance though.
I think the library approach is right. It would be nice to see the language transparently support contiguous array members, but supporting it in a library will allow more people to use it regardless of compiler. It avoids the standards approval process and implementation as well, which take non-trivial amounts of time. The 0x/1y features definitely make it a lot more feasible.
It would probably give people a better chance to play with the source if you could post a link to it somewhere. I'm not sure what you plan to license it under.
70 comments
[ 5.1 ms ] story [ 125 ms ] threadIf I stumbled across the latter version, I would just scratch my head. And I've seen sooo many projects just end up in a big code bloat just because some programmers wanted to add tons of "new" features like this that add nothing and just complexify the code for no reason.
And to me, the first version does look like someone used to C who comes to write C++ but writes their C++ like it is C. Wrapping it in the second version (as others have pointed out) is safer because it is reusable, and cleans itself up.
And seriously, did you read the initial point? The problem is that the first approach must be fully rewritten for every new class with different members that exists in your code. Given how HN is butthurt over off-by-one and out-of-buffer errors, you can't say that the first example can really be debugged easily when you have 30 of them in your code. If you only had the Mesh class in your code and nothing else similar maybe it would not be worth it.. but only maybe.
The final example is incredibly simple to use, hides the complexity in one single point and prevents you from ever making mistakes outside of it and seriously, it is not that complex.
There are two 3 wrong premises in this article (IMHO): - It makes you think that using raw pointers (so called 'low level types' in the article) is more error prone than using combined types. - It makes you think that adding abstraction removes complexity. - Makes you think that you should combine/factor everything because repeating yourself is forbidden.
These assertions are straight false.
- Adding abstraction does NOT remove complexity. It ADDS complexity, and then HIDES it. The ArrayView class has absolutely no use whatsoever. Every C/C++ programmer is used to access arrays through pointers. Creating a class for that just makes a reader wonder "what the hell is this thing?". And then you have to crawl in the code to understand what is this thing. And of course, in 3/5 years from now, this ArrayView class will have tripled in size. For something as simple as reading numbers in memory, i don't want any abstraction.
- There is nothing wrong in repeating yourself once on a while => if it improves readability! Factoring too much IS wrong. It makes reading the code a pain. When I read code, I don't want to read an abstract version of what the code does. I want to grasp the maximum possible information with the minimum noise. After reading the final version of this Mesh class, I have no idea of its memory layout. I see some ArrayView and a magical create_contiguous_memory function. I'm lost. I don't know where the memory come from, whether or not I own it, what will happen if I hand a pointer to it. Everything is hidden, and that's a pure pain to understand.
> should we be using these low level types in this particular class, or use classes that better represent what we need and leave the low level types to them?
There is no mention that we want to remove complexity. Instead, the aim of modern C++ is in fact to hide it, so that high level code can be written as simply as possible while hiding all the implementation details in classes. The code you see in this article is no different from the code you find in the implementation of the standard library; in fact since it is simpler it is even easier. Does not mean that the standard library does not have complexity - but it hides it from me, possibly with low expense. Also it never mentions pointers, the problem there is all the arithmetic which is not portable and highly prone to bugs should it ever need to be modified.
The article is not factoring. It is coalescing a feature in a simple, single purpose class which does very little, and has a very simple use case and usage. If you have no idea of the memory layout, then you need to read the class, exactly as you would if you wanted to know the memory layout of std::list or std::vector. Making a class does not prevent the complexity from existing, it just prevents you making silly mistakes while using it. It never allows you not to know your trade and forget how to program.
It seems to me you are a C guy. Really, I get what you are saying, it is just not the philosophy of C++, and if you disagree, maybe you shouldn't be using C++. That's perfectly fine, and I love that there are C people doing C things - it's not my cup of cake, but I'm happy they're there. And I would understand also if you were complaining about C++ trying to do something it is not. But what's happening here it really not the case, so I don't agree with you either.
IMO, of course.
I've seen great C++ code that I just thoroughly enjoy reading, because it doesn't make assumptions about anyones' intelligence, and I've seen C++ code that is purely designed exclusively for only the elite who are mighty enough to wield the sticks that the C++ committee provide. C++ really gives all kinds of control authority to any style you wish to use .. another of those quaint software ironies that the power that makes one weak, enlightens another ..
C++ is getting better, but its early development was driven by questionable features (iostreams, ugh) and denial that the language / runtime needed some basic features, like string and buffer types that didn't suck.
I've been using the language since 1987, and still have my own safe subset, augmented recently with some of the C11 stuff that's become solid in the toolchains I use.
But it's still a misfeature-ridden grabfest of a language.
Every C++ programmer I know uses their own narrow subset of C++. Except that those using "hardcore" ++ features appear to be far more vocal about their subset being the one true way to use C++.
Still, this article is the exact reason why I just can't stand "the new C++ trend": Code bloat for no added value, false sense of safety with fragile abstractions, pointless factoring which makes code unreadable :(
Why do you keep saying that? Do you think repeating it will make it true?
First, the computation of uvs_size is wrong; it's using sizeof(indices[0]) rather than sizeof(uvs[0]). Since the the former is an int and the latter is a Vector2 (presumably 16 bytes), the system doesn't allocate enough memory and there will be a heap overflow.
Second, it tries to do pointer arithmetic on void pointers which is undefined.
Third, it doesn't take into account alignment requirements. For example the Vector2 class would ideally be defined as 16-byte aligned so that the Vector2's can be operated on by SSE instructions. This code gets that wrong or right depending on the number of integers in the adjacent array. So in the worst case it'll be lovely code that works until a data file is changed, and then suddenly starts crashing in odd places.
Sorry, screw raw pointer arithmetic when 7 lines of code have three non-obvious errors, one of which is extremely serious. It seems like a pretty good counterargument to your claim that using raw pointers is not error prone.
I would say the code doesn't go far enough. Instead of using `std::unique_ptr<char[]>`, it should define some kind of memory_block abstraction (using `unique_ptr` internally), where the copy constructor and `operator=` both do a byte-wise copy of the data. Then alongside `make_contiguous`, you would have an analogous `copy_contiguous`, where the base case of `make_contiguous_helper` would instead call `memory_block.resize(total_size)` and the base case of `copy_contiguous_helper` would call memory_block = other_memory_block. This would remove the boiler-plate code for the copy constructor in Mesh, which can instead be:
and similar for `operator=`.In any case, having a sensible copy constructor on the memory_block (and any necessary associated data) would at least make the copy constructors in the client classes (assuming you have more than one user of this pattern) cleaner.
It's a really good pattern, though. Excellent blog post.
If you did that with the C++ code in this article there would be errors too. Major straw man.
The bug where you copy a line, need to change a bunch of spots and miss one is a common pattern. It happens even when writing real code into a real editor/IDE, and especially when dealing with code that has lots of repetition (as advocated by Galanwe). It's also usually not detected by the compiler, and detected at runtime by simple smoke tests only if you're lucky.
So I don't think it's a strawman argument. It's exactly what you'd expect to happen at some point when using a style that encourages copy-paste programming. You can easily find lots of examples of similar bugs in the wild, in production code that has been running for months or years. It's just fortunate that the example code had an instance of that bug pattern.
But also, even if I didn't have a choice and had to write the code in C++, I would do it in the style on the slide, not the style endorsed by the link on this HN thread.
I do agree that copying a line and changing spots is a common mistake pattern. However, I also still think that in many cases that is the best thing to do because it results in the simplest code. So rather than go through contortions in the actual code to try and prevent this, I am wondering if some kind of IDE pattern matching is a better way to catch this class of errors.
In generally I tried to avoid an overly negative tone when referencing your code or statements, I hope you find that it's reflected in the post.
I think that a "combined type", can be safer if well written than a raw pointer. Surely you don't disagree with that? That's not the same as saying all combined types are better.
Similarly, adding abstraction can be good, it can also be bad. C adds abstraction to assembly. I'm guessing though that you would choose C over assembly for many things?
Repeating yourself is a bad thing in general. It could be that to get rid of the repetition we'd have to incur other costs that might be worse. I don't think that's the case here.
I just don't understand your "magical create_contiguous_memory" comment. Do you feel like the sort function is magic, and instead we should write all of our sorts out at the call site? More practically, we write a good sort once, document it carefully. The users of sort know roughly how it works and exactly how to use it. In the rare cases they care, they read the code. make_contiguous is exactly in this boat: you write it once, you write it carefully, and then you use it without worrying about the details every single time. Our brains are just too small to deal with all the details all the time, that's why we're trying to hide complexity that isn't as immediately relevant.
Your post is mostly sweeping generalities which aren't true in general, and as far as talking about the code goes, you mostly just say you don't like it or find it hard to read, and brush aside anything concrete like reusability, testability, bounds checking, etc, specific things that are actually useful.
From an ok, not great programmer with a year's experience, I wouldn't necessarily expect them to fully understand make_contiguous right away. But I would expect them to understand who owns the memory, and the memory layout.
It's simpler at the expense of being more bug prone. Notice that the "simpler" struct has no housekeeping code to "delete[]" the memory. The programmer now has to manually "manage" that cleanup somewhere.
>, and the layers of abstraction added are of pretty much no value here.
The "value" he's addressing from the "simpler" but more fragile code:
1) remove bug-prone code which requires manually synchronization (copy&paste) of pointer types in both the lines defining the variable and the subsequent lines casting the pointer from a raw buffer: "mesh->positions = (Vector3 )mesh->memory_block;"
2) remove brittle manual arithmetic of pointer offsets embedded inside contiguous arrays such as: "mesh->uvs = (Vector2 )(mesh->memory_block + positions_size + indices_size);"
Calculating offsets (and/or enforcing logical boundaries) inside of arrays is very error prone and the OpenSSL Heartbleed bug was an example of that type of defect.
3) manage the release of memory using unique_ptr. Obviously, it is to help the programmer (and other coworkers using Mesh data structs) avoid memory leaks.
>features like this that add nothing and just complexify the code for no reason.
It doesn't look like "no reason" to me. The attempt was to reduce bugs, and add memory safety.
Regrettably this trend is all but new. Boost is god knows how old now and just look how popular it is in certain circles.
D represents a much better attempt at what C++ ought to have been.
Also at some point you're going to have to promote this to the GPU, at that point the original code will have a closer affinity to the GPU code rather the nonsense c++. I can forgive this though as he states he's not a games programmer.
Code like that is the main culprit of C++ is still considered an "unsafe" language, despite a correct usage of it, although more verbose and with more abstractions, brings both the benefits of C-like speed and higher level languages safety.
See, this is what I never quite understand. I use C++ when I want to do C type things. Manage my own memory. Bit twiddle with speed. Avoid all dynamic allocations. Increase cache coherency. Call inline assembler. Use the CRTP to achieve static polymorphism. Design my own containers. Lock free containers with atomics. Etc. If I'm using C++ I want nastiness. I want to get down and dirty.
Why on earth would you ever want to use it as a higher level lanuage? It sucks balls at that. Just go and use a proper, nice, well designed high level language instead. If I want a language that holds my hand and protects me from the evil pointers, I'm not choosing C++. Using C# or Java (for example) is complete luxury compared to C++.
But apparently, there are a whole bunch of people that do attempt to use C++ for this purpose. Who strive to achieve a language where you never see a raw pointer, but you are still stuck with maybe 30% of the productivity you'd have in a proper higher level language. It boggles my mind. Congratulations, now you have no garbage collection, a crap IDE, you're still wrestling with forward declarations of classes and header files and the preprocessor and linker options and slow compilation and you've now buried the machine under a layer of abstraction. The absolute worst of both worlds.
>brings both the benefits of C-like speed and higher level languages safety.
It doesn't. You end up with neither!
Except that it does. This article clearly demonstrates one way that C++ provides higher level safety without overheads - the allocated memory is guaranteed to be freed.
Nobody is saying you can't do low-level stuff in C++. They're just saying that C++ provides a zero-overhead way to do it safely. Why wouldn't you want that?
Use std::string everywhere and the (re)allocation rate / fragmentation will make your allocator very sad. C-style strings are not "fast" per-se, but at least they make all the overhead explicit.
Use std::shared_ptr for everything and you'll end up with a higher overhead than any proper GC, particularly on a multicore system.
Use templates too much and your code size will blow away any L1 cache very soon.
Use std::mutex/std::lock to protect shared state and it is going to cost you much more than synchronized in Java, which Java often turns into a noop, but C++ compiler can't.
Use virtual functions and (in all except very trivial cases) they will prevent strong opimizations like inlining in many cases where C# or Java would have no problem.
Want to have safe array access? Sure, still possible to add range checks (e.g. use at instead of []), but are you really sure C++ compilers are as strong in eliding these checks as compilers for the languages with mandatory array bound checking?
Oh, you can even go the Java/C# style very heavily and use Boehm GC. Because you can. Who said C++ is lower-level? Except it is nowhere near the performance levels achieved by proper modern GCs.
Also, not sure why if someone points to the obvious places where C++ could be slow, people treat it personally as an attack on "their" language. The fact that many high-level abstractions are not-free in C++, doesn't preclude it from being still highly performing, particularly if you know the cost of these abstractions and you know when to avoid them.
The thing about templates is misleading. Sure, if misused they can hurt your cache. But they also move branching from run time to compile time, which is a very good thing. Google some benchmarks of C++ sort vs C qsort; the former is much faster because the comparator can easily be inlined.
The templates used in my post, for example, either bloat the code not at all, or very little. They are either generating tiny functions that get inlined anyway (like ArrayView; once a function is inlined its irrelevant whether it came from a template or not for code bloat purposes) or they are generating code that would just need to be written by hand (like make_contiguous).
I've actually personally witnessed two fairly detailed accounts of people that actually got themselves into situations where template or template-like bloat became a performance negative, relative to the benefits they provided. They didn't cite a cliche about code bloat, they actually benchmarked and found the cost. What both of them were doing was very extreme; nobody who's not using very extreme template techniques (and therefore is sold on it) is hitting that point.
Cache locality. Cache coherency is something different.
In a scenario where users can access elements of a joint array of requested size without limitations, they can access memory outside the array. Memory block's total requested size may overflow size_t. Fortunately, overflow prevention is easy to add to the generic version.
What the hell are you talking about? It's a game.
> In a scenario where users can access elements of a joint array of requested size without limitations, they can access memory outside the array
Welcome to modern computing! A process can access any part of its mapped address space without limitation. Joke aside, are you suggesting a process should hide memory from itself? Forget it, I can't even make sense out of your comment.
> Memory block's total requested size may overflow size_t
Are you serious? Then please suggest a fix for C, C++, Java, and whatsnot, because nothing will prevent you in pretty much any language to ask for more memory than you can handle. This is not a _bug_, it's just expecting the programmer not to fuck around.
Here's an example with a better description: https://www.verisign.com/en_US/security-services/security-in...
>(...) could allow an attacker to execute arbitrary code with the privileges of the current user.
>The vulnerability occurs when reading the header of the Pack200 compressed Jar file during decompression. To calculate the size of a heap buffer, the code multiplies and adds several 32-bit integers. The bounds of these values are not checked, and the arithmetic operations can overflow. This results in an undersized buffer being allocated, which leads to a heap-based buffer overflow.
I remember there was a similar bug in some other decompression library.
Your comment that this is less readable is valid. Readability varies widely with background, skillset, consistency, and a host of other factors. If your team finds C-style code more intuitive, don't write Java style code. If your team finds malloc error prone, use smart pointers.
Perhaps the biggest advantage of C++ is its flexibility, at least compared to the other mainstream languages. This means everyone can write code the way they want and others can use it (a).
(a) OK. There is still a large list of gotchas around types, the preprocessor, header inclusions, namespacing, etc. that you need to know about to be a good citizen, at least until C++ can figure out how to support modules.
As a lifetime C++ guy I find this new paradigm (inline code a-la structured code) brittle and fragile. App assumptions are laced throughout; nothing can be changed without a rewrite. And my go-to code-reading techniques (follow a class through all its incarnations with grep etc) fail - they rarely subclass, just overriding existing classes locally to create callbacks etc.
So I'm not a fan of the new order. I see it's a productivity tool for the app writers out there. Especially with the one-off code that startups write, expecting to do a rewrite once they're funded. It really makes sense for them.
But I weep for the craft.
I'm curious as to the problem you're exploring/solving here?
"Concatenated struct's in single chunk" is a neat trick. The use case seems a bit broken[1], but I'm assuming that it's from the original video, and your c++-ifying it? Or at least trying to find a nicer way to write this style of code?
If you'r going this route, I'd be tempted not to use pointers at all, and just use offsets and accessors.[2]
But the basic point I'd like to make is, there is no nice way to write this type of code, because c and c++ gives us no nice[3] way of expressing it.[1] Verts and Indices are generally uploaded to a GPU then discarded, but this has the counts, which is needed CPU side for draw calls, in the same chunk so would need to be copied before freeing.
[2]
[3] Maintainable, fast at runtime, low mental friction to people other than the author, etcI guess we'll agree to disagree, I think that C++ gives us some nice ways of writing it, I think that's what the post shows :-)
Perhaps there's a reason allocators wouldn't work here, but if so, it deserves a little discussion.
I think I did give it a "little" discussion :-). Guess it depends on your definition of a little. I didn't want to talk more about it because I didn't want to get sidetracked, and this is ultimately the way I chose to do it.
Hope that sheds some light on the post and the choices I made with it.
Indices are not vertex attributes and thus should not be interleaved with the other data.
These "joint allocations" seem to be "a small pool of heterogenous types." The motivation is that heap allocations are expensive, so we want to coalesce them. The only way this would matter at all is if we are creating lots of these Mesh objects.
The given solutions make one heap allocation per Mesh object, as opposed to three. Create six million Meshes and you get two million allocations.
Instead, one pool should be created for each of the three array types: Vector2, int, and Vector3. Now, create six million Meshes and you can do somewhere between three and a few dozen allocations, depending on your pool's initial size and growth strategy.
I didn't watch the presentation so I may be wrong but it's possible you're misinterpret how Mesh was intended to be used.
The "million" objects is meant to be inside one Mesh. Mesh is singular. The vertices are plural (millions).
If there's more than one Mesh, it would be dozens or hundreds of Mesh (per video game characters).
I think it's risky to say that it's misguided without the full context of the problem.
1. Why is the goal to put everything in one block with a single allocation? Could everything still work if they were separated?
2. What do Vector2 and Vector3 look like? What if they say "int a, b" and "int a, b, c" respectively? If a perfectly-aligned "new int[total]" would have fixed the problem, it should have been tried from the start and the code refactored accordingly to not necessarily use structure types to look up the data.
3. Conversely, are Vector2 and Vector3 complex classes with special constructors (or equally important, could they someday become that way)? If so, even a "fixed" memory-allocation solution will be equally fragile to maintain because there is a responsibility to ensure that constructors are called correctly.
4. What is the memory profile of the rest of the application, e.g. how many Mesh objects themselves are created and is the entire approach to managing Mesh objects wrong? Maybe the focus on optimizing one piece has missed an entire problem somewhere else that is more fundamental.
Clearly the original code has bugs but it is also only 14 lines, the fixes for the bugs are straightforward and the right solution (after other analysis) may well have been to remove the code entirely instead of doing the same thing in a different way. Beware of the tendency to "fix" things without looking more deeply at the actual problem.
Automating the calculation of offsets and assigning pointers definitely eliminates a lot of potential bugs, but I do wonder if this isn't a bigger deficiency in C++. Why force storing an extra pointer per array? I am still not sure why C++ doesn't allow FAMs [1].
There's probably a question of which would be more efficient, storing pointers in the object and calculating the offset from the pointer, or dynamically calculating the offset within the object. Still, it doesn't seem like a problem developers should need to solve.
[1] https://en.wikipedia.org/wiki/Flexible_array_member
Regardless, I'm still not sure what the justification is requiring it be UB even for just primitive types.
The thing is, who should solve it? The different approaches you listed have different advantages, there isn't one right answer. If the language itself solves it for you, you are stuck with whatever solution the language picked. This would be fine in a higher level language, but not in C++.
You're right though that individual devs shouldn't be solving it, it should be in a library. If there's enough interest in these posts, I'm happy to put up my work (fully fleshed out and documented) on a github for people to use.
I think the library approach is right. It would be nice to see the language transparently support contiguous array members, but supporting it in a library will allow more people to use it regardless of compiler. It avoids the standards approval process and implementation as well, which take non-trivial amounts of time. The 0x/1y features definitely make it a lot more feasible.
It would probably give people a better chance to play with the source if you could post a link to it somewhere. I'm not sure what you plan to license it under.
I'll keep an eye out for the next post.