C++ / REFERENCES, POINTERS, AND NULL
void pointers and when to avoid them
Use void* to carry raw addresses across type-erased C boundaries, convert them back safely, and pick templates or variants when C++ offers a typed alternative.
What you will learn
- Convert to void* implicitly, and back with static_cast to the exact original type
- Explain why *vp, vp + 1 and sizeof(void) are ill-formed, not merely discouraged
- Write a C-style callback that recovers its typed data from a void* user pointer
- Choose templates, virtual dispatch or std::variant over void* inside C++ code
Understanding void pointers and when to avoid them
A void* stores an address and nothing else. Every other pointer type carries a second piece of information that lives only in the compiler: how wide the pointed-to object is and how its bits are to be read. With int* p the compiler can build *p (read sizeof(int) bytes and treat them as a signed integer) and p + 1 (advance sizeof(int) bytes); a void* has discarded exactly that knowledge, which is why *vp, vp + 1 and sizeof(void) are ill-formed rather than just unwise.
The trip in is safe: any object pointer converts to void* implicitly, and a static_cast back to the same type is guaranteed to reproduce the original pointer value. The trip out is where the risk lives, because static_cast<double*>(vp) is not a question but an assertion, and nothing verifies it. dynamic_cast cannot rescue you either, since it needs a polymorphic class type to examine and a void* offers no type at all, so a wrong cast reads the wrong number of bytes at a possibly misaligned address and the program is undefined from there on.
So treat void* as a boundary type, not a design tool. It is the correct and often the only choice when you talk to code compiled without knowledge of your types: std::qsort's comparator, pthread_create's user-data argument, malloc, memcpy, a C callback registry. Inside C++ the type-preserving tools are almost always better, because a template stamps out a version per type, virtual functions dispatch on the real object, std::variant and std::any remember what they hold and check on the way out, and std::span carries a length next to the pointer. The working rule is to cast back to a typed pointer on the first line of the function that receives the void* and never let one travel deeper into your own code.
<iostream>
int main() {
int i = 42;
double d = 3.5;
void* p = &i; // any object pointer converts implicitly
// std::cout << *p; // rejected: no type, so no size to read
// p = p + 1; // rejected: no type, so no stride to add
std::cout << *static_cast<int*>(p) << '\n'; // you hand the type back yourself
p = &d; // same variable, unrelated pointee type
std::cout << *static_cast<double*>(p) << '\n';
std::cout << std::boolalpha
<< (static_cast<double*>(p) == &d) << '\n'; // the round trip is exact
const int c = 7;
const void* cp = &c; // a pointer to const needs const void*
std::cout << *static_cast<const int*>(cp) << '\n';
}
A void* keeps the address and throws away the type, which moves the job of remembering what is there from the compiler to you.
Worked examples
qsort, where void* is the right answer
A C function that sorts anything must take addresses and an element size, because it cannot know the type.
<cstdlib>
<iostream>
int compare_ints(const void* a, const void* b) {
int x = *static_cast<const int*>(a);
int y = *static_cast<const int*>(b);
return (x > y) - (x < y);
}
int main() {
int v[5] = {42, -7, 13, 0, 99};
std::qsort(v, 5, sizeof(int), compare_ints);
for (int k = 0; k < 5; ++k) {
if (k) std::cout << ',';
std::cout << v[k];
}
std::cout << '\n';
}
Example explained
Line 1std::qsort was compiled without knowing about int, so it takes the element size separately and shuffles raw bytes with it.
Line 2*static_cast<const int*>(a) is where the type comes back; the comparator is the only code in the program that knows what a points at.
Line 3(x > y) - (x < y) yields -1, 0 or 1 without the overflow that x - y can produce on large values.
Line 4Pass sizeof(double) by mistake and it still compiles: nothing links the size argument to the cast inside the comparator.
The same job with the type kept
Contrasts a void* plus tag design with a template, showing what the tag is standing in for.
<iostream>
<string>
void print_via_void(const void* p, char tag) {
if (tag == 'i') std::cout << *static_cast<const int*>(p) << '\n';
else if (tag == 'd') std::cout << *static_cast<const double*>(p) << '\n';
}
template <typename T>
void print_value(const T& v) { std::cout << v << '\n'; }
int main() {
int i = 7;
double d = 0.25;
std::string s = "hello";
print_via_void(&i, 'i');
print_via_void(&d, 'd');
// print_via_void(&i, 'd'); // compiles, reads a double out of an int: undefined
print_value(i);
print_value(d);
print_value(s);
}
Example explained
Line 1print_via_void needs the tag parameter because a const void* cannot answer the question "what are you".
Line 2The commented-out call is the whole problem: it is valid C++ and would read sizeof(double) bytes out of a four-byte int.
Line 3print_value is instantiated once per argument type, so each call runs code the compiler built for the real type.
Line 4std::string required no new branch; the void* version would need another tag value and another if.
No stride, no dereference
Shows what you must supply by hand once an array address has passed through a void*.
<cstring>
<iostream>
int main() {
int arr[4] = {10, 20, 30, 40};
void* base = arr;
// std::cout << *base; // no type: nothing says how many bytes to read
// base = base + 1; // no type: nothing says how far one step is
unsigned char* bytes = static_cast<unsigned char*>(base);
int third = 0;
std::memcpy(&third, bytes + 2 * sizeof(int), sizeof(int));
std::cout << third << '\n';
std::cout << static_cast<int*>(base)[2] << '\n';
}
Example explained
Line 1void* base = arr; decays the array to int*, which converts implicitly; the element size is lost at that instant.
Line 2static_cast<unsigned char*> picks a stride of one byte, so bytes + 2 * sizeof(int) states the offset explicitly instead of assuming it.
Line 3std::memcpy takes void* and const void* precisely because it only moves bytes, and its size argument carries what the type would have carried.
Line 4The last line is the shorter route once the real type is named again: indexing an int* steps by elements, not bytes.
Important notes
T* converts to void* implicitly, but const T* needs const void*; if you use reinterpret_cast or a C-style cast to reach a plain void* and then write through it, that is undefined behaviour rather than a workaround.
void* is for object addresses only: converting a function pointer or a pointer to member to void* is not portable C++, and POSIX's dlsym relies on that conversion as a documented extension.
Common mistakes
Casting back to a different type than went in, on the theory that it is all just bytes; the read takes the wrong width at a possibly misaligned address, the behaviour is undefined, and no compiler diagnostic appears because static_cast performs no run-time check.
Expecting vp + 1 to advance one byte or one element; arithmetic on a pointer to void is rejected in C++ (it is a GNU C extension), and the two fixes differ, since unsigned char* steps one byte and the real type steps sizeof(T).
Storing new Widget in a void* and later writing delete on it; void is not an object type, so the destructor cannot be found, the Widget's resources leak, and compilers emit only a warning.
Try it yourself
Change, predict, then run
Write void swap_raw(void* a, void* b, std::size_t n) that exchanges n bytes through a small unsigned char buffer, then use it on two ints and on two doubles and print all four values before and after. Then call it with sizeof(int) on the doubles and observe that the compiler accepts it without a word.
Open the C++ workspaceCheck your understanding
You keep a void* field plus a char tag saying whether it points to an int or a double, and you cast back according to the tag. What guarantee does the language give you that the tag and the pointer agree?
- The compiler compares the tag against the pointer's original type and warns when they cannot match.
- static_cast checks the target type at run time and yields a null pointer when the tag was wrong.
- None; the tag is ordinary data, so a mismatched tag compiles cleanly and the wrong-typed read is undefined behaviour.
- Reading an int through a double* is safe as long as both fit within eight bytes on the target machine.
Show answer
static_cast performs no run-time inspection at all; the only cast that examines an object while the program runs is dynamic_cast, and it requires a polymorphic class type, which a void* cannot supply. Option 4 is tempting because size feels like the only obstacle, but an int is typically four bytes, so reading a double from it runs past the end of the object, and even with matching sizes the bits of an unrelated type would still be reinterpreted.