C++ 中古老且好用的 void* 之美与简洁
The beauty and simplicity of the good old C-style void* in C++

原始链接: https://giodicanio.com/2026/06/05/how-to-declare-a-c-plus-plus-function-that-takes-a-blob-of-memory/

作者认为,尽管现代 C++ 替代方案(如 `uint8_t*` 或 `std::span`)常被标榜为更安全,但它们往往会给代码带来不必要的复杂性和视觉干扰。 对于需要通用内存块的函数,作者支持使用传统的 C 风格 `const void*` 和 `size_t` 签名。这种方法无需频繁进行 `reinterpret_cast` 操作或编写复杂的模板,使函数调用保持简洁易读。 为了解决安全问题,作者建议使用 SAL(源代码注解语言)注解,例如 `_In_reads_bytes_`。这种方式结合了 `void*` 的简洁清晰与现代代码分析工具对内存缺陷的有效检测能力。总而言之,该文章主张在代码编写中应重视可读性与实用主义,而非盲目追求那些几乎没有实际益处的“现代”复杂性。

关于 `void*` 与 `std::span` 等现代 C++ 类型的使用,Hacker News 上的讨论揭示了开发者理念之间的尖锐分歧。 原文认为 `void*` 是一种处理原始内存数据块的简单而“优美”的方式。然而,大多数评论者将其描述为一种危险且过时的“C 语言习气”。他们指出,`void*` 缺乏来源信息、大小信息和类型安全性,这使其成为未定义行为、内存损坏和安全漏洞的常见来源。 **反对 `void*` 的核心论点包括:** * **安全性和意图:** 使用 `std::span` 或类型化指针能明确表达意图,并允许编译器强制执行边界检查,从而防止指针与长度不匹配等常见错误。 * **类型安全:** `void*` 需要使用易错的 `reinterpret_cast` 操作。批评者认为,使用适当的类型或模板可以实现更好的编译时检查。 * **历史背景:** 许多贡献者强调,现代语言(包括 Go 和 C#)甚至更新后的 C++ 标准都已经采用了“切片(slice)”或“跨度(span)”概念,以取代遗留 C 代码中常见的、手动管理的“指针+长度”模式。 虽然一些人承认 `void*` 在特定的小众用例中仍有价值,但大多数参与者得出的结论是:在现代专业 C++ 开发中,依赖 `void*` 是一种严重的反模式。
相关文章

原文

Discussing several options, starting from the good old C-style void* pointer.

An interesting question you may ask in C++ is: “How would you declare a function that takes a blob of memory as input?”

For example, think of a function that hashes some input data (using SHA-256, or whatever hash algorithm), or a function that takes some binary data and writes that to disk.

Coming from my C background, an option that came to mind would certainly be:

void DoSomething(const void* p, size_t numBytes)

You simply pass a const void* pointer to the beginning of the input memory block, and the total size of the memory block, expressed in bytes.

Then, some C++ programmer could start complaining: “Hey, why do you use the unsafe old C-style void* pointer? Use some safe explicit type like uint8_t, which clearly represents an 8-bit byte!”.

So, they propose to “step up” to the following prototype:

void DoSomething(const uint8_t* p, size_t numBytes)

Now, suppose that you want to pass to this function a custom structure, like this:

struct MyCustomData {
    ...
};

MyCustomData data;

With the original void* version, you can invoke the function simply and clearly like this:

DoSomething(&data, sizeof(data));

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.

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:

// void DoSomething(const uint8_t* p, size_t numBytes)
//
// DoSomething(&data, sizeof(data));
//
// This gives a compiler error when the function expects 
// a const uint8_t* instead of const void*, something like:
//
// Error: cannot convert 'MyCustomData*' to 'const uint8_t*'
//
// You need an explicit cast in this case!
DoSomething(
    reinterpret_cast<const uint8_t*>(&data), 
    sizeof(data)
);

Why should people complexify and uglify their C++ code with the uint8_t pointer (or std::byte), when void* works just fine??

Moreover, someone could even say: “Hey, in modern C++20, we have std::span! Use it!”

Well, congratulations for rising the complexity and noise of the code even further!

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

template <typename T>
void DoSomething(std::span<T> data)

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)

Wow. With std::span the complexity-meter bumps in the red zone and goes even higher!

Someone may suggest something like a std::span<const uint8_t>? But that’s still more complex than the initial void* signature.

Do you want a pointer to a generic memory blob? C++ has already if from C: it’s called void*! Use it and enjoy.

I really dislike this attitude of some “modern” C++ programmers, that make choices that have the effect of making the code more complex, uglier and harder to write and understand.

It seems that some people are really losing the taste for good readable code.

Some good old habit from C can still be positively used in C++, like the void* pointer and the size parameters.


BTW: As a nice addition, if you use SAL annotations, the function could be decorated a bit to help code analyzers detecting memory bugs:

void DoSomething(
  _In_reads_bytes_(numBytes) const void * p,
  _In_ size_t numBytes
);

The _In_reads_bytes_ annotation applied to the pointer parameter explicitly states that the pointer points to input read-only memory (_In_reads_), and the size of this input buffer expressed in bytes (_bytes_) is represented by the numBytes parameter.

In this way, we still keep the clarity and simplicity of the function invocation:

DoSomething(&data, sizeof(data));

while also adding pieces of information that are helpful to spot memory bugs with code analyzers and other tools.

If you want to learn more about SAL annotations, you can start reading this MSDN documentation: Using SAL Annotations to Reduce C/C++ Code Defects.

联系我们 contact @ memedata.com