HN Debrief

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

  • Programming
  • Security
  • Developer Tools
  • Open Source

The post is a C++ style argument in favor of the old C idiom `void*` plus `size_t` for functions that take a memory blob. It claims this is simpler and more readable than signatures using `uint8_t*`, `std::byte`, or `std::span`, and treats the casts required by the more explicit versions as needless ugliness. That landed badly. The core pushback was that the article starts from the wrong premise. In idiomatic C++, most functions should not accept an untyped blob at all. They should accept the actual type they operate on, or a typed byte view if the function truly only cares about bytes.

If your API really consumes bytes, take a byte range type like `std::span<const std::byte>` or at least wrap pointer and length together. If you are tempted to hash, serialize, or reinterpret arbitrary structs by raw memory, stop and make the operation type-aware unless you can prove the representation is stable and meaningful.

Discussion mood

Strongly negative on the blog post. People saw it as nostalgic C style dressed up as clarity, and thought it ignored what modern C++ types are for: expressing byte intent, preserving pointer-plus-size invariants, and preventing subtle UB when raw object representation is not the same thing as meaningful data.

Key insights

  1. 01

    Weak types let APIs rot over time

    Loose signatures are not just a one-off safety risk. They make it easier for a small helper to turn into an "everything function" that accepts more shapes, flags, and calling patterns until nobody can tell what is valid anymore. The Ruby examples made the point from dynamic code, but the same design pressure applies when a C++ API erases useful type information up front. A restrictive signature does not solve bad design by itself, but it raises the cost of dumping one more special case into the same function.

    Treat broad untyped parameters as future maintenance debt, not just present-day convenience. If a function starts gaining mode flags or alternate payload shapes, split the API before callers hard-code the mess everywhere.

      Attribution:
    • jerf #1
    • VulgarExigency #1 #2
  2. 02

    Raw struct bytes are a bad semantic boundary

    Using raw memory as the input model collapses object meaning into representation details that C++ does not promise are stable. Padding bytes, alignment, base-class layout, and multiple valid encodings for the same value all break the assumption that "same logical object" means "same byte string". That turns simple-looking operations like hashing, equality checks, and serialization into traps. The bytes may be readable, but they are often not the data you meant to define your API around.

    Before operating on object bytes, ask whether your logic cares about values or exact in-memory representation. If it cares about values, write the API against the value type and serialize or normalize explicitly.

      Attribution:
    • arcticbull #1
    • saagarjha #1
    • porridgeraisin #1
    • DennisL123 #1
    • jayd16 #1
  3. 03

    Span changes the contract, not just the syntax

    `std::span` was defended not because templates are pretty, but because it encodes the actual contract. It ties length to data, advertises non-ownership, and tells readers the function consumes a range of elements rather than an erased pointer that might later be reinterpreted as anything. That is a materially different interface from `void*`, even when both can be implemented with the same machine-level operations.

    Use a range type when the unit of work is a buffer, not a naked address. You get fewer pointer-size mismatches and a signature that survives code review and maintenance with less guesswork.

      Attribution:
    • gpderetta #1
    • VulgarExigency #1
    • stinos #1
  4. 04

    Hashing object representation is rarely the hash you want

    Checksumming a struct by memory image bakes in details that are orthogonal to logical equality. Floats can compare equal while producing different bytes. Derived-class data can leak into tail padding. Uninitialized or padding bytes can poison repeatability. There are integrity-check cases where bit-exact representation is the point, but that is a niche requirement and should be called out explicitly, not treated as the default way to hash user-defined data.

    Pick one of two hash contracts and name it clearly: value hash or representation hash. Do not let a generic `void*` API blur the distinction, because users will assume the wrong one.

      Attribution:
    • jeffbee #1
    • xigoi #1
    • fp64 #1
  5. 05

    Keep void pointers at the plumbing layer

    Several low-level comments converged on a narrower rule that actually matches practice. `void*` is tolerable when the code is doing pure plumbing, like hexdumps, C FFI shims, user-data handles, or other places where the bytes are intentionally opaque. Even then, the safer pattern is one generic low-level function plus typed wrappers or newtypes above it, so the unsafeness is centralized instead of spread across call sites.

    If you must keep a `void*` API for interop or internals, isolate it behind typed entry points. Review every new direct caller as if it were an unsafe block, because functionally it is.

      Attribution:
    • MaulingMonkey #1 #2
    • AnimalMuppet #1
    • TuxSH #1
  6. 06

    ABI and compile-time costs are real but secondary

    A minority of the technical pushback was not ideological. `std::span` can have real costs in some environments, especially around MSVC ABI choices, older language modes, and template-heavy builds. Those concerns did not rescue the article’s argument though. They only narrowed the legitimate exceptions, mostly to hot boundaries, stable binary interfaces, or old toolchains where wrapper types create measurable friction.

    Do not dismiss performance and ABI concerns out of hand, but require evidence before falling back to weaker APIs. Keep the low-level exception local to the boundary that needs it, not as a project-wide style rule.

      Attribution:
    • delta_p_delta_x #1 #2
    • usrnm #1
    • j16sdiz #1
    • pjmlp #1

Against the grain

  1. 01

    Void pointer can honestly signal opaque storage

    For some low-level code, `void*` is not confusion but candor. It says the current layer does not know or care what object lives there, only that some later stage will attach meaning. In those cases, replacing it with `std::byte*` can create false confidence because the harder problems might be alignment, lifetime, or provenance, none of which a byte type actually solves.

    Do not assume changing `void*` to a byte pointer makes an unsafe boundary safe. If the real hazards are lifetime or interpretation, document and constrain those directly.

      Attribution:
    • MaulingMonkey #1 #2
  2. 02

    Templates and wrappers do increase cost

    A few commenters argued that modern C++ safety wrappers are not free in practice. `span` brings header weight, parsing, and possible codegen or ABI overhead, while generic typed wrappers can multiply instantiations and grow code size. In codebases already struggling with build times or binary size, a plain C-style interface can be the cheaper boundary even if it is less expressive.

    Measure compile time, code size, and call overhead at real boundaries before standardizing on one style. If you need a C-like API for cost reasons, pair it with stricter internal wrappers rather than pretending the tradeoff does not exist.

      Attribution:
    • drivebyhooting #1 #2
    • girfan #1
    • ndesaulniers #1
  3. 03

    Sometimes the right type really is just bytes

    One defense of the blog’s premise was that not every blob hides a richer structure. Some functions truly only require a finite sequence of arbitrary data items, and insisting there must be a stronger domain type can be over-modeling. From that angle, the real debate is whether `void*` or a byte span is the better concrete type for that concept, not whether the concept itself is illegitimate.

    Be precise about whether your function is semantically byte-oriented or whether it is ducking a stronger type. That decision should come before the syntax choice.

      Attribution:
    • 9rx #1

In plain english

ABI
Application Binary Interface, the low-level calling and data layout rules that let compiled code from different languages or compilers work together.
FFI
Foreign Function Interface, a mechanism for calling code written in another programming language from your program.
MSVC
Microsoft Visual C++, Microsoft's compiler toolchain for C and C++.
std::byte
A C++ library type meant to represent raw byte data rather than numbers or characters.
std::span
A C++ view type that refers to a contiguous sequence of elements using a pointer and a length without owning the memory.
uint8_t
An unsigned 8-bit integer type, typically used when code wants a value that is exactly one byte wide.
void*
A generic pointer type in C that can hold the address of any object but does not itself carry the pointed-to type.

Reference links

Standards proposals and language history

Performance and ABI references

Safer C++ library practice