The beauty and simplicity of the good old C-style void* in C++
Posted by movd128 3 days ago
Comments
Comment by stinos 17 hours ago
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.
Comment by VulgarExigency 17 hours ago
Comment by zahlman 14 hours ago
...Can you give a concrete example? I've been programming literally since the 80s and that doesn't ring true at all for me.
Comment by jerf 13 hours ago
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.
Comment by VulgarExigency 13 hours 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.
Comment by antiframe 13 hours ago
Comment by VulgarExigency 12 hours ago
Comment by lostglass 11 hours ago
Comment by thomasmg 16 hours ago
> 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.
Comment by stinos 15 hours ago
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.
Comment by throwaway27448 12 hours ago
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.
Comment by addaon 36 seconds ago
No, it can only be a pointer to an object. It can't be a pointer to a function, for example.
Comment by repelsteeltje 17 hours ago
And SAL annotations aren't even C++ proper.
Comment by MaulingMonkey 13 hours ago
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!
Comment by AnimalMuppet 13 hours ago
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.
Comment by MaulingMonkey 4 hours ago
A valid concern. I've been in the position of having to fix those bugs.
I'm not going to recommend `qsort` over `std::sort` or anything like that. I've seen `void*` "user data" pointers in C APIs and decided to stuff runtime checkable handle values in there instead of real pointers to blobs. In Rust, this can mean avoiding some unnecessary `unsafe { ... }` blocks, since while reading anything from a `*const c_void` requires such things, reading from a global `Mutex<HashMap<*const c_void, Arc<dyn Any>>>` or similar nonsense does not. I'll eat the performance hit unless I'm worrying about an actual hot path!
And I'm not going to stand in the way of newtype wrappers around void pointers either. I'll +1 the PRs with `class ASpecificKindOfBlob { void* data; size_t length; };` and suchlike as well, if `std::span`s aren't your type of thing, and abide by measures to centralize such plumbing in one place such that the opportunities to make a mistake are fewer.
But sometimes a blob is just a blob, and breaking out `std::byte` is putting lipstick on a pig. And it's not even the right color lipstick.
Comment by ryandrake 13 hours ago
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.
Comment by MaulingMonkey 4 hours ago
Comment by drivebyhooting 13 hours ago
Comment by ndiddy 11 hours ago
Comment by drivebyhooting 8 hours ago
Comment by ndiddy 6 hours ago
Comment by drivebyhooting 2 hours ago
Either the objects are simple and trivially copiable, or you need a proper serialization library.
Sure you can use span to generalize slides and iteration, but I don’t think that’s the point of the article.
Comment by fluffybucktsnek 1 hour ago
In contrast, with span, you can instantiate only to span<uint_8> (or something similar) and you'd still be able to accept other buffer types (such as vector<uint_8>, array<uint_8>, etc.). Alternatively, you can make T bounded to be trivially copyable. You can't do that with void*.
Comment by throwaway27448 12 hours ago
Comment by drivebyhooting 8 hours ago
Templates have to get parsed and instantiated over and over again. Then you need link time optimization to deduplicate all the redundant copies of the same code.
Comment by fluffybucktsnek 5 hours ago
Comment by drivebyhooting 2 hours ago
Comment by fluffybucktsnek 1 hour ago
Comment by gignico 18 hours ago
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.
Comment by trumpdong 17 hours ago
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.
Comment by arcticbull 17 hours ago
struct S {
char c;
int i;
};
struct S a = {0};
struct S b = {0};
memcmp(&a, &b, sizeof(a)) == ...
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).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.
Comment by saagarjha 15 hours ago
Comment by arcticbull 40 minutes ago
For static or thread storage, in C11 and later, ={0} will guarantee padding is zeroed. For automatic storage, per C11 6.7.9, only subobjects are required to be zeroed. Padding is not. [1]
In C23 initializing with ={} will give you zeroed padding, initializing with ={0} will not.
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf
Comment by porridgeraisin 16 hours ago
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.
Comment by pjc50 16 hours ago
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 ...
Comment by trumpdong 15 hours ago
Comment by simiones 16 hours ago
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.
Comment by singpolyma3 14 hours ago
Comment by simiones 14 hours ago
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.
Comment by repelsteeltje 16 hours ago
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.
Comment by pjmlp 17 hours ago
Comment by spacechild1 17 hours 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...
Comment by pjmlp 17 hours ago
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.
Comment by spacechild1 15 hours ago
Comment by tialaramex 11 hours ago
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.
Comment by spacechild1 9 hours ago
I wrote "presumably", but you are 100% correct. I'm always happy to be proven wrong.
N3851 actually deals with multi-dimensional spans and goes way beyond a simple slice/span type. To me it seems closer to std::mdspan than std::span.
The earliest proposal I could find that does propose something similar to std::span dates back to 2012: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n33...
I really don't understand why this was not pursued further. At the very least, this should have made it into C++17 together with std::string_view.
> because of course neither of these were actually proposed as core language features even though that's what they naturally should be
Should it really? What would this even look like in C++? IMO std::span works perfectly fine as a library type.
> 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.
That's certainly not the reason why it was standardized. Pre-C++98 was wild west with every compiler offering there own (incompatible) idea of what C++ is. Yes, there are many problems with design by committee in general (and the C++ committee in particular), but there was a very good reason for standardizing the language. The committee is not a one man show and there are many occasions where Bjarne has publicly voiced his frustration and disagreement.
Comment by tialaramex 7 hours ago
Of course it isn't, all the great egotists need a parade of sycophants to heap praise on them, you've doubtless seen modern US "Cabinet meetings" in which TV hosts newly elevated to run parts of the US government compete with experienced politicians as they all try to offer the most effusive praise for their snoring God King.
Personally, I'd throw up, but then I'm very much of Groucho Marx's view on such things.
Comment by spacechild1 7 hours ago
Where exactly have you seen this "parade of sycophants" in the C++ standards committee?
As far as I know, Bjarne is just a regular committee members with just as many votes as everyone else and no veto powers. The committee frequently accepts or rejects proposals against his will. For a recent example, see his harsh criticism of the new 'contracts' feature in C++26.
Comment by tialaramex 3 hours ago
> Where exactly have you seen this "parade of sycophants" in the C++ standards committee?
AIUI The committee itself operates under the "Chatham House Rule" in which participants agree not to tell anybody who said anything and so we can only see group outcomes for the committee itself. For example 100% affirmative votes for Bjarne's "Profiles" proposal. At 100% everybody who had the opportunity to vote "Against" has to admit that er, they didn't, because that's just maths - but you won't now find anybody who was enthusiastic, somehow a room full of people who all now remember being uncertain voted affirmatively anyway. How about that.
> Bjarne is just a regular committee member
For almost a decade, WG21 has a "Direction Group" with a handful of members which insists that while as you say everybody is just a "regular committee member" their group ought to set the "direction" for the language and thus the committee. The exact membership of the Direction Group varies over time, but of course Bjarne Stroustrup has always been a member of this group. The group (whatever its present membership) writes only unanimously, which means everything it says has been agreed by Bjarne Stroustrup, and it cites as its reference for how to set the direction several books about C++ all written by that same Bjarne Stroustrup.
So, sure, Bjarne is "just a regular committee member" in the same way that Britain's Prime Minister is "just a regular Member of Parliament" that is, very much in theory but not at all in practice.
Comment by pjmlp 14 hours ago
Which by your comment, you have no clue about how it came to be.
Proposal is linked in another comment of mine.
Comment by spacechild1 9 hours ago
Comment by delta_p_delta_x 17 hours ago
Sadly the MSVC ABI makes std::span and std::string_view a pessimisation:
Comment by usrnm 16 hours ago
Comment by j16sdiz 16 hours ago
Comment by usrnm 15 hours ago
Comment by delta_p_delta_x 14 hours ago
Will never happen on Windows, especially not in user-mode libraries, and especially not something this pervasive.
Comment by pjmlp 14 hours ago
Which is why Valve ended up using Proton.
Comment by gpderetta 12 hours ago
GCC was forced to break the std::string ABI by the C++11 standard and they have been lobbing ever since against ABI breaks.
Comment by wahern 11 hours ago
Comment by pjmlp 12 hours ago
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.
Comment by locknitpicker 17 hours 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.
Comment by pjmlp 16 hours ago
The C++ span proposal came from Microsoft,
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p01...
Comment by locknitpicker 15 hours ago
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?
Comment by pjmlp 14 hours ago
Governments,related cybersecurity agencies, and companies are the ones getting outraged when looking at money spent in cyber attacks due to memory corruption issues.
Comment by wahern 10 hours ago
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.
Comment by wahern 6 hours ago
Comment by uecker 2 hours ago
Comment by wahern 1 hour ago
Comment by locknitpicker 13 hours ago
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.
Comment by pjmlp 12 hours ago
Comment by uecker 2 hours ago
Comment by gignico 15 hours ago
Comment by locknitpicker 15 hours ago
You're missing the fact that following C++98 it took around 13 years to get the next version of the standard published delivered.
Comment by IX-103 16 hours ago
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.
Comment by AnimalMuppet 13 hours ago
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.
Comment by eddd-ddde 12 hours ago
Comment by TuxSH 8 hours ago
Thus void*+size is usually the right type if ones only care for the memory representation of an object (cstring functions like memcpy, etc.)
Most likely one would have both overloads:
void Hexdump(const void *p, size_t size); // (1)
template<typename T> // (2)
inline void Hexdump(const T &obj) {
return Hexdump(&obj, sizeof(T));
}
With (2) being a wrapper to (1) that compilers will almost always inline, avoiding monomorphization costs (and (2) can also accept rvalues as argument).(1) could also take std::span<const u8>, but (void*, size) is the more common idiom, more convenient to use and to read , as it is unambiguous which overload it is.
Comment by kstenerud 12 hours ago
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.
Comment by scott01 12 hours ago
Comment by okanat 10 hours ago
Comment by fp64 12 hours ago
Comment by kstenerud 12 hours ago
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.
Comment by fp64 11 hours ago
Comment by xigoi 12 hours ago
Comment by fp64 11 hours ago
Comment by throwaway27448 12 hours ago
Comment by 9rx 12 hours ago
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.
Comment by locknitpicker 12 hours ago
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.
Comment by cherryteastain 14 hours ago
template <typename T>
void DoSomething(T const& data);
or if T* is supposed to point to a tightly packed buffer template <typename T>
void DoSomething(std::span<T> data);
as the author pointed out. I don't see how that is ugly or more complicated than the original void* approach.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.
Comment by delta_p_delta_x 17 hours ago
> 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.
Comment by pjmlp 16 hours ago
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.
Comment by repelsteeltje 16 hours ago
Comment by pjmlp 16 hours ago
Comment by voidUpdate 18 hours ago
> "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
Comment by trumpdong 17 hours ago
Comment by mfost 17 hours ago
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.
Comment by adrian_b 13 hours ago
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.
Comment by AnimalMuppet 13 hours ago
Comment by AnimalMuppet 13 hours ago
(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...)
Comment by adrian_b 13 hours ago
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.
Comment by gpderetta 12 hours ago
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.
Comment by Ailrk 1 hour ago
Comment by rfgplk 13 hours ago
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.
Comment by arcadialeak 17 hours ago
Comment by quietbritishjim 17 hours ago
void DoSomething(void* src, void* dst, size_t numBytes);
... 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*.(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).
Comment by myrmidon 16 hours ago
Comment by mwkaufma 14 hours ago
Comment by s28l 14 hours ago
Comment by mwkaufma 12 hours ago
Comment by jurschreuder 7 hours ago
To make sure I would put it in some kind of container.
Comment by newsoftheday 12 hours ago
To me, that is a feature, not an issue.
Comment by akkaygin 16 hours ago
Yes.
Comment by delegate 17 hours ago
Dynamic languages can handle this with reflection, but with void* you can only pray nobody makes the mistake..
Comment by zahlman 14 hours ago
Comment by s28l 14 hours ago
Comment by zahlman 14 hours ago
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.)
Comment by pjmlp 14 hours ago
So instead of anything goes, there is some additional type checking depending on the type of cast being made.
Comment by ndesaulniers 12 hours ago
Comment by jayd16 14 hours ago
Would anyone argue yes?
Comment by nine_k 12 hours ago
Comment by DennisL123 12 hours ago
Comment by girfan 7 hours ago
Comment by arka2147483647 17 hours ago
All cpp alternatives are more wordy.
I wonder how this conversation wound go if the was an as terse, but also typesafe cpp alternative.
Comment by pwdisswordfishq 16 hours ago
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.
Comment by void-star 13 hours ago
… Is why I picked my name.
Comment by themafia 17 hours ago
Isn't there a way to make this an alias anyways?
Comment by singpolyma3 14 hours ago
Comment by jeffbee 13 hours ago
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.
Comment by drysine 13 hours ago
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.
Comment by squirrellous 17 hours ago
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.
Comment by api 16 hours ago
Comment by _the_inflator 17 hours ago
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.
Comment by zahlman 14 hours ago
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.
Comment by repelsteeltje 16 hours ago
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.
Comment by simiones 15 hours ago
Comment by zahlman 14 hours ago
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.
Comment by bcjdjsndon 15 hours ago
Comment by pjc50 16 hours ago
Comment by rfgplk 13 hours ago
Comment by bcjdjsndon 15 hours ago
Comment by pjmlp 13 hours ago
Comment by trumpdong 15 hours ago
Comment by adev_ 17 hours ago
> 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.
Comment by sylware 16 hours ago
Comment by Uptrenda 18 hours ago
Comment by bcjdjsndon 16 hours ago