- > Why should people complexify and uglify their C++ code with the uint8_t pointer (or std::byte), when void* works just fine??
Fair point (although to be honest: 'complexify' feels a bit of an exaggeration here to me), but the answer to this why is simple: document and express intent clearly. The compiler gave you an error first such that you're forced to consider what you're doing. Any seasoned C++ developer seeing this knows what this reinterpret_cast means.
> Wow. With std::span the complexity-meter bumps in the red zone and goes even higher!
Same remark: yes, it's a bit more text to read, but again: to me (and many others I'm guessing) this clearly expresses intent. I also do not find it particularly hard to read. I mean, it's C++, you're likely going to encounter templates at one point or another, except in super specific software perhaps. But no-one also ever argued the C++ learning curve was easy, and trying to make it easier by refusing to use features which were added for good reasons and instead going back to constructs which are the very source of those reasons seems a bit backwards.
> As a nice addition, if you use SAL annotations, the function could be decorated a bit to help code analyzers detecting memory bugs
Some might also say it complexifies and uglifies the code. And in any case makes it non-portable on top of that.
- It seems unlikely that this is the case, as the author appears to be experienced, but the post reads like the author has never had to maintain a "simple" and "beautiful" function that was mangled into incomprehensibility over the years, and where if a more expressive type signature had been written from the start, it would have restricted the damage caused over time.
- > a "simple" and "beautiful" function that was mangled into incomprehensibility over the years, and where if a more expressive type signature had been written from the start, it would have restricted the damage caused over time.
...Can you give a concrete example? I've been programming literally since the 80s and that doesn't ring true at all for me.
- I can't, as my employer owns the code, not me, but there are several examples in one of the Ruby codebases I unfortunately maintain where I can see this degeneration happen via the git history. A small 8 line method with just two parameters slowly grows in complexity over time, until one day one of the original parameters supports two different shapes, and later on it's not that easy to understand which shape it should have in the specific conditional branch you're trying to fix, and the last person to touch that code left the company 4 years ago.
The fault, of course, ultimately lies with the people who wrote and approved this nonsense, but types, or at least type hints, help to avoid this issue.
- Can you point to it? That doesn't sound like "the language forced all this extra baggage on it due to 'safety'" so much as the developers kept adding functions to the function without rethinking if and how they should.
- My point was not about the safety of the code, it was about the expressiveness, which is also what the comment I replied to was about. If the parameter has an explicit type (instead of no type, as is normal in Ruby, or `void*`, which is the C equivalent), it forces the developer to consider the design of the function, instead taking the path of least resistance because they're inexperienced/incompetent/a large language model/burnt-out to the point where even the thought of opening the file makes them feel the not-anxiety of burnout/<insert reason here>.
- Forcing developers to consider and more is harmful though. You're arguing to put all of the forethought upfront, when you have the least context and least understanding of what can go wrong, and carrying that complexity forward rather than starting simple and refactoring over time.
- Have you been in static types the whole time? It's a really, really common failure state in dynamic programming languages, the Everything Function, that started out as something simple, but then someone added a flag to make it also do this other thing, and then you need a flag to only do that other thing sometimes, and someone needed to operate on multiple things so they made a string parameter also optionally an array, and later someone allowed it to also be an object with this one method, or maybe another method if it's present because some other team implemented that before the first one and can't switch now, and before you know it it's a free-for-all of people adding flags and options and type analysis and if statements and you have a complete mess. Especially if this function is shared by many disparate teams, each of whom isn't "allowed" to break the others, though a single team can fail this way plenty fine.
You can still do this in static languages, but they do push back a bit more because you don't get the flexibility that dynamic languages offer when it comes to accepting a huge variety of different input types.
I've torn a few of these apart over the years. Never fun. Haven't tried with AI but suspect that would only be a quantitative change rather than a qualitative change. The fundamental problem with fixing these is lack of information about the exponential complexity of possible call mechanisms and the AI will have the exact same information problems I will, just faster.
Edit: One of them that I tore apart ended up being two entirely separate functions slammed together into one by historical contingency. I don't just mean that I broke the functionality down into multiple functions, that's a basic tool of how you tear these down and is nothing of note. I mean that one of the "everything functions" I tore down had two distinct calling patterns that were distinct functions that not only shouldn't have been festooned with so many options, but never should have been one function at all because they weren't even conceptually the same thing or even particularly related.
Think of it as two stages of a straight-line process, that were just jammed together because of the fact they got called at similar times, and the original writers weren't clear on the unrelated nature of the tasks and nobody was able to see it through all the obfuscation until I sat down, very deliberately, and I realized this as I was tearing it apart. I don't remember the details, I tend to remember things very conceptually and thus I have a hard time remembering the details of functions with no conceptual purity, but you can get close by thinking of the function as validating incoming parameters, and then applying the parameters to a database. And people were so confused that despite the fact this function, when tickled correctly, could do it all in one shot, sometimes, kinda, with some caveats, there were places where this function was called first to validate (with flags to shut off the application), and then to apply (with flags to shut off the validation). And to be clear, I mean, I did not realize it either even from my contact with the function over the years. It was only when I sat down with it for hours and systematically tore it down that I figured that out.
- I don't have a strong opinion what is better in this case, but my view is:
> document and express intent clearly
Arguably, the void* does that as well?
> Any seasoned C++ developer seeing this knows what this reinterpret_cast means.
Same for void*?
> it's a bit more text to read
If you have to call it many times, this adds up.
> Some might also say it complexifies and uglifies the code
I think the point is that it adds security, which the other options don't. And, it doesn't add complexity on the caller, but only at one place: the implementation.
> makes it non-portable on top of that.
This can be solved.
- > Arguably, the void* does that as well?
Sort of (I mean: seeing void* and a size probably means 'arbitrary sequence of bytes' or something like that, but well it's void* so it can be like anything whereas with std::span you get more of a hint what's going on just based on the type), but not at the callsite which is what the author is referring to when it's about reinterpret_cast.
> I think the point is that it adds security, which the other options don't
Imo span also does that to some extent, but already when writing the code and not afterwards in e.g. static analysis. E.g. if I get an std:span<const char> I'd have to do counterintuitive things to misuse it. Annotating a void* still leaves it a void* which I then need to cast to char* if I think that's what it is intended.
Don't get me wrong: I've written my fair share of void* but these days I really feel like there's almost always a better thing which can be used instead. Though I do admit that since I've written and consumed a lot of code with such alternatives I'm not hindered by readability/apparent complexity of it anymore but I understand that's not the same for everyone.
- > Arguably, the void* does that as well?
How do you figure? The type is a pointer to quite literally anything, including nothing (ie a pointer that cannot be dereferenced). If you're working with bytes, indicate this with the type.
- +1
And SAL annotations aren't even C++ proper.
- > Fair point (although to be honest: 'complexify' feels a bit of an exaggeration here to me)
Both uint8_t and std::byte require a header (<cstdint> or <cstddef>) which may expose you to platform x config specific build failures if you do any conditional #including, and the latter is a whole damn enum class with a strange adversion to arithmetic, where `byte |= 1` becomes `byte |= std::byte(1)`, `byte += 1` becomes `byte = std::byte(std::to_integer<std::uint8_t>(byte) + 1);`, and both become something you can accidentally step into in your full debug builds because it's an actual function call (at least on MSVC - still extra instructions on clang/gcc, but I can see the dang call instruction on MSVC!) instead of a compiler built in.
Not to mention, neither is vanilla C++03... I threw a `std::byte` example in a quick godbolt snippet and MSVC wouldn't compile without adding /std:c++17, because of course it defaults to earlier. Which is silly, but that's also the story of my life.
And don't get me wrong - that's all relatively minor - but it's all for middling to negative value IME. `void*` is frequently clearer - it's a signal that it's an opaque blob at this point in the code, and that something else will try to give it meaning later. I struggle to think of a single bug that I've encountered, that would've been caught by the compiler had I used `std::byte` over `unsigned char` or `void`. And conversely, I've seen APIs accepting `std::byte` but requiring higher alignment, where with `void` I might not have dropped my guard as much.
> `std::span`
At least manages to bind pointer and size into a single variable, which IME at least has the advantage of eliminating some bugs (e.g. mismatching pointers and sizes) and allowing some nifty utility functions to become a lot more wieldy. You can do things like feed it an array and not have to do any of your own `sizeof(...)` shenannigans. At this point you're possibly getting into positive expected value, but I'm going to eye roll at pull requests refactoring `void*` based stuff to use it unless I see at least one actual concrete example of calling code improving alongside it - I don't want just hypothetical theoretical ergonomics, I want actual concrete ergonomics!
- > `void*` is frequently clearer - it's a signal that it's an opaque blob at this point in the code, and that something else will try to give it meaning later.
And that's fine, until something else gives it the wrong meaning later. If you're just plumbing, and you're pumping around opaque blobs, if somewhere in the plumbing you connect the wrong source to the wrong destination, you get no warning.
- Yes, in this case, void* is kind of smelly. If the intent of your function is to receive a const struct MyCustomData*, then that should be the type of the argument. If you later need to handle a const struct MyOtherCustomData*, you can add an overload that takes that argument. Or use a template as others pointed out. Use the type system to help you, so you're warned if you try to pass the method const struct BadCustomData* by accident.
If you truly don't know what the underlying structure of the "blob" of data is, sure, go ahead and use void* and explicitly convert the pointer type when you know what it is, but at least add a comment that you're entering the danger zone.
- Span will increase compilation time for no useful reason. It’s not any safer at the call site.
- Span lets you use a ranged for loop to iterate over the contents without worrying about exceeding the bounds, which is safer than pointer+size if that's all you'll be doing. C++26 also introduces .at() for span, and the new hardened standard library enforces bounds checking when using operator [] on a span.
- Well if compilation time is an issue, you chose the worst possible language to use. But if you must use C++, you should use the mechanisms that best communicate intent.
- The real question here is: WHY are you passing a blob of memory rather than a struct that uses the type system to describe and enforce what the contents are?
I don't mean dressing up an anonymous pointer, which the author rightly complains about. I mean WHY are you making an API that takes such a pointer to an unknown type to begin with? Whenever you change the structure within that blob, your type checker won't flag that the receiver hasn't been updated to handle it.
Even worse: nothing's stopping you from accidentally passing in the wrong type.
And now you have a SEGV. Or a security hole.
- I think the article names hashing as a use-case, which I can somewhat still agree. Operations that only depend on the bytes, I guess. But yeah, most things worth saying about this article have been said here already
- Sure, if the function is expected to not treat the data as anything but bytes, then it might be acceptable in narrow circumstances.
But in such a case I'd argue FOR the ceremony, as a way of declaring from the API "The input is a sequence of bytes that I won't treat as anything other than a sequence of bytes", and declaring from each and every call site: "This is not a mistake; we really are 'converting' this struct to a series of bytes for this function to consume".
Then anyone auditing the code knows the intent by the shape of the types, and would quickly flag any typecasting shenanigans within the receiver function.
But even then, hashing a struct will rapidly bring you into the land of dragons and fairies. Abandon all hope if you have floats or UTF-8 (which have multiple representations for the same values).
Far better to remain type-aware if you value your sanity.
- I agree, the original article is rather questionable. I do not write code like the article advocates for. I would probably go for overloads for each data type I have considered and tested, or maybe something fully templated, or std::span/boost::span (hash function is, interesting enough, the very example boost docs give to illustrate boost::span).
- Hashing everything based on the byte representation breaks when you have a type where equality does not imply byte equality. Such as… floats (+0 and -0 are equal, but have different byte representation).
- Depends on the use-case, hashing can also be used for checking integrity/change in which case you exactly want the behavior that only bit-exact-equality is desired, even for arbitrary structs. Maybe that's somewhat niche, I mention it as I have such a use-case actually.
- Even then, accepting a uint8_t* would make this intent clearer.
- > Whenever you change the structure within that blob, your type checker won't flag that the receiver hasn't been updated to handle it.
The relevant type is "blob". There is no further structure. If the function that accepts void* is trying to extract structure out of the blob, there is a bug in that function and the type checker should already catch you trying to extract structure from something that isn't there.
> I mean WHY are you making an API that takes such a pointer to an unknown type to begin with?
It's not unknown in any meaningful sense. It is known to be a sequence of 'arbitray' datums of a given length, which is the exact type of input required for the scenario given.
As the article explores, some argue that you should define that sequence with a concrete type, but the article states that it doesn't offer any additional value as is posits that void* already communicates the same. In other words, it suggests that void* is the concrete type for that type already.
- > The real question here is: WHY are you passing a blob of memory rather than a struct that uses the type system to describe and enforce what the contents are?
I completely agree. It's particularly egregious when the blogger complains that the complexity and ugliness lies in the type casting to force an incompatible type where it doesn't belong, and use a reinterpret_cast of all things.
This doesn't even feel like a strawman argument anymore. This sounds like a coding horrors entry.
- > It seems that some people are really losing the taste for good readable code.
It seems that some people never had taste for good reliable code. Use `void ` and now any error whatsoever is a direct undefined behavior. Moreover `std::span` clearly says that you are not* taking ownership of the memory (even though the language does not check it of course), while `void *` does not.
I understand that people can have many things to say about C++, and I do as well, but `std::span` should have been there decades ago and is such a life saver in these situations. A truly zero-cost abstraction which effectively saves you from a lot of troubles.
- There's lots of UB in C-family execution models. Some of which is not actually UB because the implementation defines it - e.g. aligned DWORD-sized memory access is atomic on Windows because Microsoft said it is.
By choosing to use this language you choose to navigate the UB. Otherwise you'd be writing in Go, or Python.
It is possible to write reliable code despite the presence of UB in a language just like it's possible to drive to work every day for 20 years despite most of the directions you can point the car leading to an immediate crash. That's a needle with a much thinner eye than UB in C, and most people manage it. Mainly it means being very careful about lifetime and ownership. The Linux kernel manages it 99% of the time simply by being careful about lifetime and ownership, and that's a project with a huge number of contributors who don't intimately know each other's modules. I'm the Linux kernel you can't just say "new whatever" - you must have a plan for a lifetime of that whatever, and other people will review it.
I agree with you about std::span.
- Yeah but also, quick question:
If you answered 0, you'd be wrong, the answer is undefined, thanks to padding, initialization and alignment rules. Padding bytes are undefined, and not guaranteed to be initialized to zero even if the variable is declared static (where the members would be zeroed).struct S { char c; int i; }; struct S a = {0}; struct S b = {0}; memcmp(&a, &b, sizeof(a)) == ...This is why the compiler is angry at the post writer, and why the reinterpret_cast is needed. Ideally if they wanted to do something with the data, they'd unbox the structure.
That's why it's not a good idea to use void* to pass arbitrary data interchangeable with bytes. It's a location, it makes no representation as to what's there and how to interact with it. Let alone who owns it.
std::span solves two problems here. One is the ownership problem. The other is that span<T> is a T[]. void* is god only knows.
The post asserts:
> The code is very clear and straightforward: you pass a pointer to the custom data structure, and its size in bytes. That’s it. Simple and clear.
This is unfortunately entirely false in C thanks to the aforementioned alignment/padding UB (and of course inner pointers). This is addressed with std::span. You'd still have to reinterpret_cast your structure to get the UB.
> Why should people complexify and uglify their C++ code with the uint8_t pointer (or std::byte), when void* works just fine??
tl;dr: because it doesn't. It just kinda looks like it does if you squint, and it's going to lead to the gnarliest bugs in the world.
- Padding bytes are initialized to zero if you zero initialize the aggregate. It is hard to keep those bytes as zero but at initialization this much is guaranteed.
- > even if the variable is declared static
No, for static even padding bytes are zero.
For automatic, yes it may effectively turn a = {} to a.member = 0, leaving the padding bytes uninitialised. Or on copies like a = b it may not copy padding bytes.
- > Some of which is not actually UB because the implementation defines it
No - if something is UB in the spec, it's UB. The implementation will do something, sure, but what it does is not fixed and may even change based on compiler version and optimization level.
> DWORD-sized memory access is atomic on Windows because Microsoft said it is
Well, Intel said it is. Mind you I don't think there are any 32-bit native architectures where aligned dword access isn't atomic. Unaligned, on the other hand ...
- "Undefined behavior" in the C standard literally means "behavior which this C standard does not put any requirements on" - it says so in the definitions section of the C standard. Other things can still put requirements on it. MSVC isn't just a C++ compiler - it's a C++ compiler for x64 Windows and therefore follows the rules of C++, x64, and Windows all at once.
- > No - if something is UB in the spec, it's UB.
A compiler is still free to ignore the spec and declare that something is not UB. However, this is very much compiler based, not platform based. Windows might guarantee that aligned DWORD-sized memory accesses are atomic, but that doesn't mean Clang when compiling for Windows would respect this - but MSVC might.
- No, a compiler obviously cannot do this. nothing is undefined behaviour under a known compiler, version, and settings. UB means you can't know what the code does in general not that you can't know what it does in a very specific case.
- UB has 2 very different implications:
1. It means that even if your program happens to work, it can't be portable
2. It means that even if your program happens to work today, it might stop working tomorrow when you add some new code, when you change some compiler flags, or when you do even a minor compiler upgrade
Of course, a compiler can't address 1. However, a compiler can very much address item 2. If Microsoft were to say "in MSVC, we define integer overflow to wrap", then they would guarantee that `INT_MAX + 1` will produce `INT_MIN` regardless of any optimization settings, any compiler upgrades, any other changes to the code. Of course, compiling the exact same program with Clang or GCC might cause it to crash or corrupt memory or anything else - but as long as you stuck with MSVC, your program would have perfectly defined semantics.
This is similar to using compiler extensions or intrinsics - they are not portable and not defined by the standard, maybe even explicitly defined to NOT be supported per the standard (such as variable length arrays in C++ in GCC), but they are nevertheless perfectly safe as long as you stick to your chosen compiler.
Edit to add: the integer overflow example is not just a theoretical possibility - lots of C++ compilers provide the `-fwrapv` flag; when using that flag, signed integer overflow is no longer UB for that program, it is defined just the same as unsigned integer overflow.
- There is a difference between UB in C, and something being undefined in some version of Microsoft C on Windows.
Many of C's UB is specifically, intentionally left undefined in the standard to express code that relies on some specific way it is handled, is not proper, portable C. Indeed, the DWORD-sized memory access being atomic doesn't apply to MS Windows prior to version 3.0 running on a 80286.
It's UB because the ISO C spec says it's UB.
- That is quite common among C developer culture, play loose and brace for impact.
- > but `std::span` should have been there decades ago
Absolutely! I now use it consistently in all new projects where I can afford to mandate C++20. I guess nobody bothered to make a proposal before...
- They did in C, from one of the language authors even, and it was not accepted.
https://www.nokia.com/bell-labs/about/dennis-m-ritchie/varar...
By the way, both Extended Pascal, Mesa/Cedar and Modula-2 have them, under the name of open arrays.
Basically it took Go, C# and others for C++ to finally get its span.
C probably never will.
- Everybody knows that C++ did not invent the concept of spans and that it was late to the party. It doesn’t change the fact that (presumably) nobody made a proposal to the C++ standard.
- > It doesn’t change the fact that (presumably) nobody made a proposal to the C++ standard.
There were proposals about this for many years. C++ is just a terrible programming language, standardized by a committee (WG21) which exists in large part to boost the ego of one man, Bjarne Stroustrup.
N3851 for example wants to name this idea "array_view" which like "string_view" is an impressively unwieldy name for a core language feature, because of course neither of these were actually proposed as core language features even though that's what they naturally should be -- but it is basically the slice type or as you (and modern C++) call it a "span".
It's true that you can't change facts but what you've got here was a belief which was unfounded, not a fact.
- Microsoft made the proposal for C++, after Midori project, and Office security improvements.
Which by your comment, you have no clue about how it came to be.
Proposal is linked in another comment of mine.
- > A truly zero-cost abstraction
Sadly the MSVC ABI makes std::span and std::string_view a pessimisation:
- Sounds like a compiler bug to me. It is a valid reason to avoid them in some rare cases right now, but it doesn't make the feature itself bad
- Those are ABI. Unless it is inlining them, the overhead is to stay.
- ABI changes do happen. gcc had an ABI change in std::string because of C++11. It was long and painful, but everyone survived, the world did not end
- > ABI changes do happen
Will never happen on Windows, especially not in user-mode libraries, and especially not something this pervasive.
- Contrary to the FOSS compile from source culture, other platforms have a different point of view on ABI breaks.
Which is why Valve ended up using Proton.
- I'm pretty sure GCC has been ABI stable far longer that MSVC which used to break ABI every release.
GCC was forced to break the std::string ABI by the C++11 standard and they have been lobbing ever since against ABI breaks.
- GCC/libstdc++ just changed the ABI for std::variant: https://gcc.gnu.org/gcc-16/changes.html#libstdcxx
- You haven't used Windows in a while I imagine.
MSVC has stabilised the ABI since VS 2015, we are on VS 2026 now.
Due to customer pressure to stop doing exactly that, to the point some ISO C and C++ stuff that requires breaking the ABI has not been implemented thus far.
I am quite certain that I will find ABI breaks in GCC release notes since Slackware 2.0, when I used it for the first time.
- > I understand that people can have many things to say about C++, and I do as well, but `std::span` should have been there decades ago (...)
Decades is kind of a stretch. C++11 introduced smart pointers, and finally getting C++0x out of the door was already a major victory. Given the history of C++, it would be unrealistic to introduce something like std::span before C++17.
Meantime, some organizations are still struggling to migrate to something like C++14.
- It could have been there since the beginning, given that open arrays (aka spans) already existed in other languages, and there was even a failed proposal from Denis Ritchie regarding C.
The C++ span proposal came from Microsoft,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p01...
- > already existed in other languages
This argument is moot. The issue with spans is not that they require cutting edge technology to deliver.
Before commenting, perhaps you should research why even Denis Ritchie himself could not sell his idea to C.
It's funny how every single idea that's rejected is blindly lauded as brilliant but silenced due to some kind of conspiracy, and only the ideas that emerged are somehow bad, unacceptable, or late. Is the point to feel outraged?
- Easy, even one of the author's could not change WG14 mind towards security.
Governments,related cybersecurity agencies, and companies are the ones getting outraged when looking at money spent in cyber attacks due to memory corruption issues.
- WG14 adopted variably modified types, a kind of dependent type. From a security standpoint it offers all the same qualities. It also in principle was easier to integrate from a backwards compatibility standpoint, with the exception of struct member analogs (which we now have but aren't yet standardized).
Maybe we would have been better off with Ritchie's counter proposal. But neither proposal was chiefly concerned with security, thus no proposals for, e.g., automatic bounds checking.
- > Easy, even one of the author's could not change WG14 mind towards security.
Your comment conveys a hefty dose of ignorance on the topic. I recommend you read the proposal's arguments, including how it required breaking the ABI.
- Are you asserting that WG14 never had the necessary skills among all the members to help improve this proposal, or dare to bring another one during the last 40 years?
- Afaik std::span does not need anything that was not in C++98 already, or am I missing something?
- > Afaik std::span does not need anything that was not in C++98 already, or am I missing something?
You're missing the fact that following C++98 it took around 13 years to get the next version of the standard published delivered.
- Ick. The entire article starts from the fundamentally flawed premise that "you want a function that takes a blob of memory as an argument". Then they discuss bytewise access into structures..
Passing around void pointers is simply not a safe thing to do in C++. You can't do anything with a void pointer, so you're probably going to cast it as something else. Use that type instead, so that your caller knows they need to pass a valid pointer to that type. If the pointer has the wrong alignment then that will result undefined behavior. If you need to support multiple pointer types, use templates.
And, unless there are some really weird circumstances, you actually don't want to access your structures bytewise. Offsets can shift with compiler flags/versions. If you want serialization , please use a serialization library that correctly handles all of the odd cases. These can be quite efficient.
I've only actually had to munge bytes in a class once. Somebody decided that a previously POD class that was passed between processors with different memory spaces needed a virtual function, so I had to overwrite the vtable when I received it to make it valid.
- In general, you're right.
The exception that I could think of is a "dump memory" function. You take a pointer to something (who cares what it is), and print out the bytes there. That I could see taking a void*.
But that's a really limited case. In general, yes, you do not want to be dealing with blobs of memory as arguments. You want to be dealing with things that are known to be the right kind of thing as arguments.
- But parent is right, you have to cast it anyways before reading from it, so might as well take the right type from the beginning.
- In C++ you'd just do
or if T* is supposed to point to a tightly packed buffertemplate <typename T> void DoSomething(T const& data);
as the author pointed out. I don't see how that is ugly or more complicated than the original void* approach.template <typename T> void DoSomething(std::span<T> data);There is no need to pass the size of T or length of the span, former is just a sizeof(T) away and latter is a data.size(); away.
In fact, a lot of codebases would outright ban the uint8_t* and reinterpret_cast trick the author is complaining about via clang-tidy rules.
- The "void*" of C solves a frequently encountered problem, but it has an inappropriate and misleading name, because such a pointer points to something, it does not point to nothing. Moreover, it does not have a size.
The correct solution in a programming language is to have a primitive bit string type (with a length that is a byte multiple) and to have a concise way (e.g. with dedicated symbolic operators) to write a type conversion from any data type to a bit string and a type conversion from a bit string to any data type.
Then the operations that make sense for arbitrary bit strings, e.g. copying, moving, input/output operations (e.g. file read and write), applying Boolean functions, shall have formal parameters of this type.
Much of what I have described here already existed in the language IBM PL/I, more than 60 years ago, except that in it only the conversion towards a bit string was explicit, with the built-in function "bit", while the conversions from bit strings to other data types were done implicitly, upon variable assignment.
Like any kind of array, a bit string must have an associated size, so there should be no need to specify it explicitly as a separate parameter.
- > "An interesting question you may ask in C++ is: “How would you declare a function that takes a blob of memory as input?”"
> "Now, suppose that you want to pass to this function a custom structure, like this:"
You would create another function that actually works based off that structure, rather than using your first function which operates on a set of bytes in memory. That way it's readable, like they want, and type-safe
- I find this to be a snarky non-answer. You really think everyone should write their own memcpy for every POD type they want to memcpy?
- There's no need: there's std::copy already.
Or maybe the idea was to create a typesafe template wrapper around the generic function which is also very common and really nice. No need to create one wrapper per type, a single template should work.
- And how was std:copy implemented?
Your answer is valid only for a programming language which assumes that the standard library is implemented in another language.
C/C++ are supposed to be languages in which any program can be written, unlike languages like Java or Python.
- Yes, C++ has those parts. You can use them to write something like std::copy. But use them, once, to write std::copy, and get that one implementation right (or have the library writers do it for you), and then use std::copy everywhere, rather than using void* everywhere. This makes your program much more type safe and less buggy, while still letting you write the parts where you really need to use the down-and-dirty stuff.
- In addition to what mfrost said, there's also no need because C++ assignment is member-by-member copy unless otherwise specifically implemented for the type. If you have a POD, then that's what you get with assignment; there's no need to call memcpy at all.
(The difference is that memcpy will copy padding bytes, and the assignment operator may not. But if you depend on the values of the padding bytes, you have major problems...)
- The blogger and the blog says:
> BTW: As a nice addition, if you use SAL annotations
> Windows C++ Programming
Not everyone will see the irony, but the Windows user-mode application and library suite and the kernel now very heavily rely on the safety mechanisms of C++ that the author calls 'complex', 'uglif[ied]', and has 'los[t] the taste for good readable code'. I'm of course referring to the Windows Implementation Library: https://github.com/microsoft/wil This is explicitly an effort from MS WinDev to make Windows C++ code safer. User-mode applications writing native Windows code can and absolutely should use it, too.
Any time I see `void*` in C++ I ring-fence it as a C-ism and make sure I `reinterpret_cast`. For me, a bag of bytes is `std::span<std::byte>`. void* is a memory location with no provenance, no ownership, no size information, nothing. Do I even know if it is this program's memory, or some shared memory construct, or maybe even a pointer into GPU memory? No for all.
C likes to play fast and loose and its proponents call it 'beautiful and simple', I call it a segfault/use-after-free/double-free waiting to happen.
- It goes even further their beloved C code is compiled in compilers written in C++, including the standard library, exposing C++ implementations as extern "C" functions.
It is a pity that Microsoft backtracked on their C support.
WWDC is happening this week, one set of announcements at State of the Union was how Apple replaced a few C, Objective-C and C++ components, including at OS level with Swift.
- Interesting. I'm know nothing about Apple, but maybe you can explain how idiomatic Swift handles Blobs and how that interfaces with C or C++ around void ptrs, std::spans etc.?
- Those are unsafe buffers, and have specific primitives to handle them, Swift also has span, and interoperability with Objective-C and C++ code.
- > void DoSomething(const void* p, size_t numBytes)
would be something like
template <typename T> void DoSomething (const T& ref) or void DoSomething(const T& ref, size_t numBytes) or C++20-y void DoSomething (const auto& ref)
If the class you're passing in already qualifies a size like member fn, template<typename T> requires requires(T t){ t.size(); } void DoSomething(const T& x){ ... x.size(); }
> void DoSomething(const uint8_t* p, size_t numBytes)
This is awful you lose type info irreversibly.
> template <typename T> void DoSomething(std::span<T> data)
You can do this but the above examples work just as well.
> Or maybe something even more complicated, like this?
template <typename T, std::size_t N> void DoSomething(std::span<T, N> data)
// Or this? template <typename T, std::size_t N> void DoSomething(std::span<const T, N> data)
This is more explicit, not more complicated...
> In this way, we still keep the clarity and simplicity of the function invocation: > DoSomething(&data, sizeof(data));
Stripping types is not a good idea, especially because you'll run into object lifetime issues _REALLY QUICKLY_. You need to guarantee that the object is trivially copyable.
- Making DoSomething a template because span is a template is a non-sequitur.
If DoSomething works with untyped bytes, it should require a std::span<byte> (or const byte if read only). Incidentally the standard provides a convenient as_bytes(std::span<T>)->std::span<byte>; There isn't an as convenient helper to convert a singular object to a span of bytes, but it is easy to write.
As to why one should use span, is that a) it helps making sure that the size travels together with the pointer for some additional safety, b) it is more convenient to work with byte ranges than void ptrs (which do not support pointer math), c) helps a bit communicating intent: in C++ void* are used more often for type erasure than for byte related things.
- > On the other hand, with the “safe and modern” uint8_t prototype, the function call gets more complicated, as you need to add a type cast
To me, that is a feature, not an issue.
- If you know you want bytes -- A void* of unknown provenance cast to anything other than char* is UB so just skip the middleman and use char*.
- char* can also be a C-style string. std::byte has the same special treatment in the standard as char and unsigned char, with the added benefit of not being used for other purposes (i.e. ASCII character or uint8, respectively).
- I was trying to appeal to OPs maladaptive "C++ that looks like C is more legitimate" aesthetic preference, haha.
- char* is an exception to strict aliasing rules of C++ precisely to facilitate the author's use case. You would still need a reinterpret_cast to make it work, but it's actually good because it makes the intent clearer, and the cast would have still happened either way to read the raw bytes.
- That was my first instinct too, but nothing the author said indicates they actually need non-strict aliasing. If the function had been:
... then it would be a different matter since maybe you want to allow src and dst to alias. Although, even then, they're still allowed to alias so long as the function accesses them both through char*, so the function signature can still use void*.void DoSomething(void* src, void* dst, size_t numBytes);(Going deeper, non-strict aliasing applies to any pointers of the same type passed to a function. So if src and dst were both cast to float* inside the function, and if they really are both of that type (technically "an object of type float exists at the pointed-to location) then they can still alias. The char* exception is the only case that you can access a memory location through two different types of pointer and they can still alias.)
It's interesting the author mentions uint8_t. It's certainly more explicit than char, but it doesn't have the same aliasing guarantee (very strictly speaking - in practice it's almost always an alias for unsigned char or char, which does).
- This is actually pretty annoying in embedded programming in C, because you'd often really prefer to use a uint8_t buffer[] for serialization functions (e.g. to write arbitrary data on some bus etc.) over char*, but you'd actually lose the aliasing permissiveness that you need (if you are strictly sticking to the standard-- this is often ignored in practice).
- I find this point to be generally why C can typically beat C++ in terms of code size; generic functions operating on void* are much less type safe, but the tradeoff is code size. Those template instantiations add up.
- <snark> Arguably the one thing C++ is great at is its type system. Makes total sense to cast it away. </snark>
- I first wanted to compare the use of void * to the use of a chainsaw. But then I realized that a chainsaw was many more safety features.
- Here's a thought experiment. Is void* something we should add to other languages?
Would anyone argue yes?
- Many years ago I remember going through the Boost library and seeing C-style casts that seemed entirely gratuitous. I tried replacing them with what I was pretty sure were the equivalent C++ reinterpret_casts, and the result didn't compile. I never did figure it out.
- C style cast can be either a static_cast or reinterpret_cast, but it can also be a const_cast or a static+const or reinterpret+const. Finally, it will perform a static_cast that bypasses private inheritance (because the alternative would be to fall back to a reinterpret_cast, which is wrong if the static_cast needs to apply an offset to the pointer)
- > a static_cast that bypasses private inheritance (because the alternative would be to fall back to a reinterpret_cast, which is wrong if the static_cast needs to apply an offset to the pointer)
That may well have been it, then. I would think that if it could have been expressed naturally, it would have been.
(I don't think I ever used private inheritance in my own C++ code. I'm not a huge fan of inheritance at all, so.)
- To add on top of sibling comment, C style casts are too lose, and the main reason for the new C++ style casts is improved type safety.
So instead of anything goes, there is some additional type checking depending on the type of cast being made.
- > In fact, std::span is a class template, and somebody would suggest to make the function that processes the generic memory blob a function template! Really? Something like this??
Yes.
- It depends on what your function does with that memory. If the fn expects any kind of structure at that address, you and your callers are on your own, compiler can't help if the caller passes the wrong thing. Worse, acessing that memory might not immediately crash, but lead to strange side effects in your program.
Dynamic languages can handle this with reflection, but with void* you can only pray nobody makes the mistake..
- The best part of void* is that it is very terse. Both in definitions, and in access.
All cpp alternatives are more wordy.
I wonder how this conversation wound go if the was an as terse, but also typesafe cpp alternative.
- > “Hey, why do you use the unsafe old C-style void* pointer?
Exactly, one should avoid unnecessarily erasing pointer target types. Luckily, C++ gives much better tools for that than C. This should have been a tem—
> Use some safe explicit type like uint8_t, which clearly represents an 8-bit byte!”
Sigh. Out of the frying pan, into the fire.
- This!
… Is why I picked my name.
- While I sympathize with the aesthetic theme of this post, I warn against the temptation to do what the post proposes, which is to try to compute the checksum of an object represented by void pointer and extent. There are many dangers here, one of which is that the checksum may read uninitialized memory, making the checksum meaningless. Another is that the pointer implicitly converted to void may be a different address depending on the type of the object in the calling function, if the type has multiple base classes. Further, your void reader may be reading derived class data you were unaware of, such that hashing a Base pointer twice yields different results because a member of Derived was placed by the compiler in the tail padding of Base.
In other words, don't do this. C++17 introduced has_unique_object_representations type trait which tells whether it is safe to do this to a given type. It is pretty much always false.
- Next realize you can just use C instead of C++ at all!
- >a function that hashes some input data (using SHA-256, or whatever hash algorithm)
Along with padding bytes.
> Why should people complexify and uglify their C++ code with the uint8_t pointer (or std::byte), when void* works just fine??
That was the intention of reinterpret_cast - make ugly code look ugly.
- I'm not a fan of C++ precisely because of template noise but what you gain with span, in that the pointer and the length are joined together, seem to outweigh the complaints on style.
Isn't there a way to make this an alias anyways?
- One could argue the reinterpret_cast makes the intent more explicit which is a good thing.
That said I don’t have much against the use of void* or even char* here. If it works in C, it works in C++ just fine. std::span is not the right tool for this.
- Real programmers use uintptr_t for pointers.
- I think that the author is right in everything he says and yes, there is beauty in it.
However, the antithesis is also correct that there exist better solutions to solve the issues.
Both premises hold true.
I have an extensive assembler coding background on 6510, M68000, and i486. I had a very hard time accepting that something could be solved faster and more stable in a higher order language while the downside is more memory, more CPU etc.
More and more it turns out that programming languages are something accidentally read by machines and written by humans, even though this premise got destroyed lately by AI.
However, what I love about C++ is, that it has a basic canon of commands that can be used to build nearly everything while looking extremely ugly and hard to grasp if you don't read very slowly and accurately - so it is a very error prone and dangerous thing that rightfully got substituted by better constructs that allow for better distinctions as well as usage.
I could do everything in assembler (Hey Python users: you know that in the end everything ends up as machine code, don't you?) but it takes 100x times longer and is constantly reinventing the wheel.
Have you ever started to get into the intricacies of bit signs? No? Well, you should definitely, and to this day it gave me a lasting impression when I started wrapping my head around it, when I was 10 to 11 years old hacking my way into the world of assembler programming on C128.
You don't want to take every concept into consideration. You don't want to take interoperability into consideration. All the time!
You want to focus on the problem to solve, not the implications of the implementations all the time.
I am having such a blast very often using Python since it just works with much cognitive distraction about which language construct to use in order to get the machine doing what you want. It is so capable, enable it, to simply ensure within boundaries that the compiler uses the best decision given the context, which is up to analysis.
That's why I stopped using C++ or more precisely stopped any attempts and trying to be smart or fancy. I got to re-read and maintain the code month to years later and history showed, I don't marvel at how magic the line works and brutally smart I was at the time, but simply hate me for obscuring something in a line, that could be well understood if I had used 10 lines, while the compiler gives a damn anyway.
C++ is still necessary but every discussion to this day is about the point you made: every digit counts - and also which position, context etc. You got to be very prolific in order to put into a line what other put into 10.
Is it worth it? No.
In early days it was the correct decision. Memory was sparse, CPU power slow, and the language was small compared to today.
The last time I felt comfortable with a "assembler kind feeling" was with JavaScript before ES6. Peak jQuery level, with the most coolest concept only JavaScript has: Function.prototype.toString()
John Resig will have his place in my programming heroes olymp, who revealed this secret for me, and it opened my eyes for the beauty of higher order languages.
I admire C++, but so do I Python.
But I hope I won't have to ever use C++ again.
- > Hey Python users: you know that in the end everything ends up as machine code, don't you?
I don't understand where you're trying to go with this call-out, especially if you're also describing yourself as a Python user.
But, like, no, not really; ordinarily, Python is bytecode-compiled and then the bytecodes are interpreted. There's machine code doing the interpretation, but that interpretation is not transformation.
- There will always be cases (like audio processing, car brakes, pace makers) where hard real-time constraints prohibit GC languages (as well as l1 cache, instruction reordering and other optimizations). Also, consider that Python's performance frequently originates in it's bindings to libraries written in C, C++, Fortran, Rust.
I recently ran a few Java benchmarks and found that an array holding a bunch of objects incurred approx 3x the number of bytes compared to the expected number based on underlying class data structure. With current RAM prices, that is something to consider if you're building software that's meant to scale. Mileage may vary, but I expect JavaScript or Python will be similar.
So, sure. There is a case to be made that ergonomics and dev velocity. And premature micro optimizations might take your focus away from good systems architecture. But I've frequently found the need to peal of leaky abstractions and having to understand and be savvy at low level stuff too. Nothing wrong with studying the guts of a C64 or Amiga, today.
Python, Java or TypeScript are good educational tools, but you'd be doing yourself a disservice if you'd confine yourself to them without understanding computers.
- Note that, while complex, there exist GCs that can handle both soft real time and even hard real time constraints - especially for Java. Memory overhead is a problem with GC languages, though, and that one is by design.
- > There will always be cases (like audio processing, car brakes, pace makers) where hard real-time constraints prohibit GC languages (as well as l1 cache, instruction reordering and other optimizations). Also, consider that Python's performance frequently originates in it's bindings to libraries written in C, C++, Fortran, Rust.
Sure. And every Python programmer who has any interest in those use cases learns about the issues quickly. Or more to the point: a big chunk of them are things you'd only do if you were employed to do them, and employers are setting the language requirements already. And Python programmers in particular are well aware of compiled-language bindings; that's the reason they're trying to use the packages that make package installation non-trivial.
Huge swaths of use cases don't require performance.
> Python, Java or TypeScript are good educational tools, but you'd be doing yourself a disservice if you'd confine yourself to them without understanding computers.
This is an extremely strange thing to say when replying to someone who just described having extensive experience with C++ and multiple flavours of assembly.
> I recently ran a few Java benchmarks and found that an array holding a bunch of objects incurred approx 3x the number of bytes compared to the expected number based on underlying class data structure.
It was also strange to say that if you also had this experience yourself. A solid "understanding of computers" would have given you a better mental model of what Java needs to allocate. Results like this are because "the expected number" was not well thought out.
> if you're building software that's meant to scale.
... And yet everyone just keeps pumping out Electron apps. Curious, that.
- What's your point here? The higher the language level, the less performant they are? Why are using anything but assembler then?
- One of these days I want to do a "typesafe macro assembler" that actually is the language people think that C is.
- I'm actually working on something similar to this. Basically works as an additional preprocessed layer over C++. You can get some crazy results, but it's tricky to integrate with existing build tools without causing havoc.
- You could call it rust and just rename the remaining UB as "unsafe" and call the problem solved
- It already exists, MASM and TASM, and related derivatives, with high level control flow pseudo instructions and less UB.
- You could even give it identical syntax to C!
- [dead]
- [flagged]
- This post post is honestly speaking a bag of garbage and ill advises:
> Some good old habit from C can still be positively used in C++, like the void* pointer and the size parameters.
That's garbage.
There is a clear interest of passing both size AND pointer in a single parameter like `std::span<std::byte>: It bind both value together and guarantee that you do not mess with the size of your buffer.
Pass "data" and "size" parameters through a chain of 5 function calls and there is a non-null probability that you passed "other_size" instead of "size" somewhere. This pattern happens everywhere in old C codebase and has been the source of countless security vulnerabilities and random buffer overflows for decades.
All modern languages (including freaking minimalist Golang) have now a "slice/span" concept built in.
It is not just to annoy programmers (and allow them to complain about 'complexity' in blog posts) but because it is a major improvement in term of memory safety and in term of reducing user errors.
> It seems that some people are really losing the taste for good readable code.
If 'span<std::byte>' or 'span<char>' are unreadable for you. The problem is not span, the problem is you.
These are concepts that has been existing for decades in almost all modern programming languages.
Even in conservative C++, it exists since 2014 in the GSL, in Qt and in boost.
And the interface is no different from vector...no excuse here... It is itself the most basic data-structure in C++.
> Why should people complexify and uglify their C++ code with the uint8_t pointer (or std::byte), when void* works just fine??
Sure. Let's extend the logic: I do propose also to replace all typed arguments with a void* pointer.
Because after all: 'It will just works fine' right ?
Type-safety and clear interface are overrated, we could all use only bytes and remove interface all together to get a closer experience of Fortran 77.
/irony
> Or maybe something even more complicated, like this? > template <typename T, std::size_t N> void DoSomething(std::span<T, N> data)
First that is non-sense.
If you want to pass a mutable buffer of byte, the correct signature is:
``void DoSomething(std::span<std::byte> data)``
There is no need for template signature here. You are making things up.
Second, there is also no need for the N parameter
``span<Type,N>`` is only used when enforcing a buffer with its size known at compile time is desirable. It can be for vectorization (e.g buffer is a multiple of the SIMD line) or to make it explicit in the interface (e.g for bloc cipher for instance)
> states that the pointer points to input read-only memory (_In_reads_)
You do that by using `std::span<const std::byte>` in any C++ codebase.
The fact he brags about that as "an advantage" for separated parameter passing just show currently how little is known here.
> My Pluralsight Courses
The kind of C++ code proposed in this blog post would be straight be refused in any PR in almost any serious organization with a proper review process.
So bragging about it on a blog while proposing some C++ teaching is audacious to say the least.
> To finish on that.
The sad thing is that there would be very valid criticism on `std::span<std::byte>`:
- Span does not do boundary check on access by default. Which is a bad design decision in 2026.
- It has an impact on compilation time due to the header inclusion
- std::byte is annoying to work with because it is a hack around an enum instead of a proper C++ builtin type.
But the blog post misses all these points entirely and sticks to complaining about 'Old C being better' the same way your family Grand-Uncle still brags about 'lead gasoline being better' for his 70s Pontiac.
- Makes you wonder why OP is using cpp to begin with if theyre suggesting void*