The beauty and simplicity of the good old C-style void* in C++

Posted by movd128 3 days ago

Counter66Comment145OpenOriginal

Comments

Comment by stinos 17 hours ago

> 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.

Comment by VulgarExigency 17 hours ago

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.

Comment by zahlman 14 hours ago

> 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.

Comment by jerf 13 hours ago

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.

Comment by VulgarExigency 13 hours ago

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.

Comment by antiframe 13 hours ago

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.

Comment by VulgarExigency 12 hours ago

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>.

Comment by lostglass 11 hours ago

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.

Comment by thomasmg 16 hours ago

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.

Comment by stinos 15 hours ago

> 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.

Comment by throwaway27448 12 hours ago

> 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.

Comment by addaon 36 seconds ago

> The type is a pointer to quite literally anything

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

+1

And SAL annotations aren't even C++ proper.

Comment by MaulingMonkey 13 hours ago

> 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!

Comment by AnimalMuppet 13 hours ago

> `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.

Comment by MaulingMonkey 4 hours ago

> if somewhere in the plumbing you connect the wrong source to the wrong destination, you get no warning.

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

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.

Comment by MaulingMonkey 4 hours ago

To be fair, the `void*` is already a pretty big hint that you're in the danger zone.

Comment by drivebyhooting 13 hours ago

Span will increase compilation time for no useful reason. It’s not any safer at the call site.

Comment by ndiddy 11 hours ago

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.

Comment by drivebyhooting 8 hours ago

The caller still needs to construct the span correctly.

Comment by ndiddy 6 hours ago

You can pass both C arrays and some STL containers (i.e. std::vector, std::array) into a function that takes a span, and the span will get constructed automatically. You have to construct it manually if all you have is a pointer and a length, but I don't know what you'd expect to happen there.

Comment by drivebyhooting 2 hours ago

It’s not semantically safe to pass arbitrary vectors into a generic buffer copying function. The T in a vector<T> could have internal pointers or worse things.

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 comparison to a plain void* and a separate size, it's still an improvement. As others mentioned, void* suffers from the same problem (it might point to a type that is not trivially copyable), except it has more opportunities for mistakes.

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

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.

Comment by drivebyhooting 8 hours ago

It’s this kind of attitude that perpetuates bad compilation time.

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

If youif you are using C++, your last concern will be compilation times. By this point, just use C.

Comment by drivebyhooting 2 hours ago

Untrue. Lots of effort is spent optimizing compilation time at big FAANG companies. And there a lot of established techniques for creating “compiler firewalls” and explicitly instantiating templates once.

Comment by fluffybucktsnek 1 hour ago

But these compilation times optimizations don't significantly undermine the other goals. Given that we're talking about std::span<T>, a pretty small template all things considered, I think practical evidence (e.g. actual cases) of impact is needed.

Comment by gignico 18 hours ago

> 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.

Comment by trumpdong 17 hours ago

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.

Comment by arcticbull 17 hours ago

Yeah but also, quick question:

  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

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.

Comment by arcticbull 40 minutes ago

I looked into it some more and it's actually worse.

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

> 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.

Comment by pjc50 16 hours ago

> 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 ...

Comment by trumpdong 15 hours ago

"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.

Comment by simiones 16 hours ago

> 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.

Comment by singpolyma3 14 hours ago

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.

Comment by simiones 14 hours ago

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.

Comment by repelsteeltje 16 hours ago

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.

Comment by pjmlp 17 hours ago

That is quite common among C developer culture, play loose and brace for impact.

Comment by spacechild1 17 hours ago

> 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...

Comment by pjmlp 17 hours ago

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.

Comment by spacechild1 15 hours ago

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.

Comment by tialaramex 11 hours ago

> 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.

Comment by spacechild1 9 hours ago

> There were proposals about this for many years.

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

> The committee is not a one man show

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

Are your seriously comparing the C++ standard committee to the Trump administration? I know you have an axe to grind, but this is getting ridiculous.

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

Yes, I am seriously making that comparison. It's not as bad of course but it's certainly enough to make me cringe.

> 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

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.

Comment by spacechild1 9 hours ago

Well, you could have linked an actual proposal instead of dropping some cool facts about C, Extended Pascal, Mesa/Cedar and Modula-2, as if that explained anything.

Comment by delta_p_delta_x 17 hours ago

> A truly zero-cost abstraction

Sadly the MSVC ABI makes std::span and std::string_view a pessimisation:

https://github.com/tringi/win64_abi_call_overhead_benchmark

https://godbolt.org/z/7baaox7re

Comment by usrnm 16 hours ago

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

Comment by j16sdiz 16 hours ago

Those are ABI. Unless it is inlining them, the overhead is to stay.

Comment by usrnm 15 hours ago

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

Comment by delta_p_delta_x 14 hours ago

> ABI changes do happen

Will never happen on Windows, especially not in user-mode libraries, and especially not something this pervasive.

Comment by pjmlp 14 hours ago

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.

Comment by gpderetta 12 hours ago

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.

Comment by wahern 11 hours ago

GCC/libstdc++ just changed the ABI for std::variant: https://gcc.gnu.org/gcc-16/changes.html#libstdcxx

Comment by pjmlp 12 hours ago

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.

Comment by locknitpicker 17 hours ago

> 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.

Comment by pjmlp 16 hours ago

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...

Comment by locknitpicker 15 hours ago

> 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?

Comment by pjmlp 14 hours ago

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.

Comment by wahern 10 hours ago

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.

Comment by wahern 6 hours ago

Just to be clear, I often think we would have been better off with Ritchie's proposal, assuming it would have seen at least as much adoption in implementations and usage as variably modified types, which sadly remained poor for many years after C99, and arguably still poor. But being better off doesn't mean being in a drastically better situation than we are today from a security perspective. The proposed alternatives were prerequisites for substantively improving security, but far from sufficient. And the delay in adopting and refining variably-modified types has cost much more than whatever marginal benefit Ritchie's proposal offered. Ditto for other gaps, like better facilities for handling arithmetic, e.g. overflow and mixed type comparisons. The first step in addressing overflow only came with C23 (overflow checking routines), and the latter only in the forthcoming C2y (typesafe, mixed-signedness min/max, etc).

Comment by uecker 2 hours ago

The support for variably modified types is excellent, if you discount MSVC which is lacking support for modern C anyway (it seems to catch up a bit though).

Comment by wahern 1 hour ago

Real-world usage certainly remains poor. Using pointers to VM types remains annoying, and I wish the committee would settle on a solution to the ordering of VM parameters. But, yeah, the VM types are solid in GCC and clang and should be used more.

Comment by locknitpicker 13 hours ago

> 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.

Comment by pjmlp 12 hours ago

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?

Comment by uecker 2 hours ago

This has not much to do with skill. Standardization does not work like this, and I told you this before.

Comment by 12 hours ago

Comment by gignico 15 hours ago

Afaik std::span does not need anything that was not in C++98 already, or am I missing something?

Comment by locknitpicker 15 hours ago

> 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.

Comment by IX-103 16 hours ago

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.

Comment by AnimalMuppet 13 hours ago

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.

Comment by eddd-ddde 12 hours ago

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.

Comment by TuxSH 8 hours ago

Anything can be aliased by char, unsigned char, std::byte (as well as signed char in C), and usually uint8_t == unsigned char, thus by extension any valid void pointer can be cast to u8*.

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

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.

Comment by scott01 12 hours ago

For type erasure (it’s sometimes useful), custom allocators, I/O, for example.

Comment by okanat 10 hours ago

Then pass an uint8_t with size aka a span<uint8_t>.

Comment by fp64 12 hours ago

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

Comment by kstenerud 12 hours ago

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.

Comment by fp64 11 hours ago

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).

Comment by xigoi 12 hours ago

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).

Comment by fp64 11 hours ago

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.

Comment by throwaway27448 12 hours ago

Even then, accepting a uint8_t* would make this intent clearer.

Comment by 9rx 12 hours ago

> 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.

Comment by locknitpicker 12 hours ago

> 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.

Comment by cherryteastain 14 hours ago

In C++ you'd just do

    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

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.

Comment by pjmlp 16 hours ago

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.

Comment by repelsteeltje 16 hours ago

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.?

Comment by pjmlp 16 hours ago

Those are unsafe buffers, and have specific primitives to handle them, Swift also has span, and interoperability with Objective-C and C++ code.

Comment by voidUpdate 18 hours ago

> "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

Comment by 17 hours ago

Comment by trumpdong 17 hours ago

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?

Comment by mfost 17 hours ago

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.

Comment by adrian_b 13 hours ago

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.

Comment by AnimalMuppet 13 hours ago

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.

Comment by AnimalMuppet 13 hours ago

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...)

Comment by adrian_b 13 hours ago

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.

Comment by gpderetta 12 hours ago

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.

Comment by Ailrk 1 hour ago

Lots of ways to criticize modern c++ but I don’t think void* vs span is a valid one.

Comment by rfgplk 13 hours ago

> 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.

Comment by arcadialeak 17 hours ago

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.

Comment by quietbritishjim 17 hours ago

That was my first instinct too, but nothing the author said indicates they actually need non-strict aliasing. If the function had been:

   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

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).

Comment by mwkaufma 14 hours ago

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*.

Comment by s28l 14 hours ago

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).

Comment by mwkaufma 12 hours ago

I was trying to appeal to OPs maladaptive "C++ that looks like C is more legitimate" aesthetic preference, haha.

Comment by jurschreuder 7 hours ago

I would argue that you would not really know what the compiler would be up to with the memory void* points to.

To make sure I would put it in some kind of container.

Comment by newsoftheday 12 hours ago

> 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.

Comment by akkaygin 16 hours ago

> 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.

Comment by delegate 17 hours ago

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..

Comment by zahlman 14 hours ago

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.

Comment by s28l 14 hours ago

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)

Comment by zahlman 14 hours ago

> 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.)

Comment by pjmlp 14 hours ago

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.

Comment by ndesaulniers 12 hours ago

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.

Comment by jayd16 14 hours ago

Here's a thought experiment. Is void* something we should add to other languages?

Would anyone argue yes?

Comment by nine_k 12 hours ago

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.

Comment by DennisL123 12 hours ago

<snark> Arguably the one thing C++ is great at is its type system. Makes total sense to cast it away. </snark>

Comment by girfan 7 hours ago

Partly serious, partly in jest: so type systems are no good?

Comment by arka2147483647 17 hours ago

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.

Comment by pwdisswordfishq 16 hours ago

> “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.

Comment by 14 hours ago

Comment by void-star 13 hours ago

This!

… Is why I picked my name.

Comment by 17 hours ago

Comment by themafia 17 hours ago

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?

Comment by singpolyma3 14 hours ago

Next realize you can just use C instead of C++ at all!

Comment by jeffbee 13 hours ago

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.

Comment by drysine 13 hours ago

>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.

Comment by squirrellous 17 hours ago

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.

Comment by api 16 hours ago

Real programmers use uintptr_t for pointers.

Comment by _the_inflator 17 hours ago

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.

Comment by zahlman 14 hours ago

> 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.

Comment by repelsteeltje 16 hours ago

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.

Comment by simiones 15 hours ago

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.

Comment by zahlman 14 hours ago

> 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.

Comment by bcjdjsndon 15 hours ago

What's your point here? The higher the language level, the less performant they are? Why are using anything but assembler then?

Comment by pjc50 16 hours ago

One of these days I want to do a "typesafe macro assembler" that actually is the language people think that C is.

Comment by rfgplk 13 hours ago

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.

Comment by bcjdjsndon 15 hours ago

You could call it rust and just rename the remaining UB as "unsafe" and call the problem solved

Comment by pjmlp 13 hours ago

It already exists, MASM and TASM, and related derivatives, with high level control flow pseudo instructions and less UB.

Comment by trumpdong 15 hours ago

You could even give it identical syntax to C!

Comment by adev_ 17 hours ago

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.

Comment by sylware 16 hours ago

[dead]

Comment by Uptrenda 18 hours ago

[flagged]

Comment by bcjdjsndon 16 hours ago

Makes you wonder why OP is using cpp to begin with if theyre suggesting void*