2

Lets assume that we have following code:

struct VeryComplexStruct {
  // a very complex struct

  void test() {}
};

void foo(VeryComplexStruct **ptr) {
  // do something
}

int main() {
  VeryComplexStruct *p = nullptr;
  foo(&p);
  p->test();
}

I wonder if the code is still valid by C++ standard if we just use void* and cast it like this:

struct VeryComplexStruct {
  // a very complex struct

  void test() {}
};

void foo(VeryComplexStruct **ptr) {
  // do something
}

int main() {
  void *p = nullptr;
  foo(reinterpret_cast<VeryComplexStruct **>(&p));
  (static_cast<VeryComplexStruct *>(p))->test();
}

This may happen if we use C libraries in C++. Any idea?

Afshin
  • 8,839
  • 1
  • 18
  • 53
  • 3
    I would rather try the opposite, use the proper type in the normal case and when needed cast to or from `void*`. – Some programmer dude Aug 21 '22 at 10:13
  • @Someprogrammerdude I normally do so too. But there are some cases (for example creating a C++ wrapper for a C library) that you want to **hide** internal data structures. In these cases, I thought maybe I can use `void*` rather than that structure's name. – Afshin Aug 21 '22 at 10:15
  • AFAIK, it's perfectly legal to cast pointers to and from void*. The error is if you ever dereference a pointer with a different type than it actually has. So by that reasoning your code is OK. – john Aug 21 '22 at 10:18
  • @AdrianMole oops, my mistake... the first one need to be `reinterpret_cast`. Just updated. – Afshin Aug 21 '22 at 10:20
  • @john I made a mistake and first cast cannot be `static_cast` since it is double pointer. it needs to be `reinterpret_cast`. I wonder if it is valid... – Afshin Aug 21 '22 at 10:22
  • 2
    "perfectly legal to cast pointers to and from void*." --> Note that this does not apply to function pointers. – chux - Reinstate Monica Aug 21 '22 at 10:27
  • @chux-ReinstateMonica that one is one of rare `void*` casts that is valid with `reinterpret_cast` as far as I'm not mistaken. – Afshin Aug 21 '22 at 10:29
  • @Afshin A function pointer may be wider than a `void *`. Casting does not certainly recover lost info and form a correct function pointer. – chux - Reinstate Monica Aug 21 '22 at 10:32
  • @chux-ReinstateMonica what I mentioned was based on cpp ref. Not sure if it is same in standard too or not: *"On some implementations (in particular, on any POSIX compatible system as required by dlsym), a function pointer can be converted to `void*` or any other object pointer, or vice versa. If the implementation supports conversion in both directions, conversion to the original type yields the original value, otherwise the resulting pointer cannot be dereferenced or called safely."* Anyway this is not that related to my question.... – Afshin Aug 21 '22 at 10:35
  • @Afshin "was based on cpp ref" --> That is a POSIX spec, not CPP one. Converting function pointer to/from `void*` may work in POSIX, yet this post is not tagged POSIX and that ability is not certain in CPP in general. – chux - Reinstate Monica Aug 21 '22 at 10:40
  • 3
    @Afshin `static_cast` is probably fine, I’d rather worry about the `reinterpret_cast`. IIRC pointers to different types are not even guaranteed to be of the same size. And you probably don’t need to do it this way anyway. You can just use incomplete types to hide implementation details. – user3840170 Aug 21 '22 at 10:46
  • Doing `reinterpret_cast(&p)` and then accessing `void **` via `VeryComplexStruct **` is undefined. – KamilCuk Aug 21 '22 at 11:07
  • `reinterpret_cast` is considered to be a code smell. There are other methods available, e.g. https://rules.sonarsource.com/cpp/RSPEC-3630 – Den-Jason Aug 21 '22 at 11:25

2 Answers2

1

if the code is still valid

The code, as presented, (ignoring that p points to nullptr and assuming p points to a valid VeryComplexStruct object) is valid - you can convert between pointer values. However, if you would access the pointer:

struct VeryComplexStruct {
    void test() {}
};

void foo(VeryComplexStruct **ptr) {
  *ptr; // this here
}

int main() {
  void *p = new VeryComplexStruct();
  foo(reinterpret_cast<VeryComplexStruct **>(&p));
  (static_cast<VeryComplexStruct *>(p))->test();
}

That would be invalid - you can't access void ** pointer with VeryComplexStruct ** handle, so it kind of defeats the whole purpose. You have to access void ** using void ** handle, like reinterpret_cast<VeryComplexStruct*>(*reinterpret_cast<void**>(ptr)).

Any idea?

Static typed languages is a feature to help you with types - using void * you are throwing it all out the window. Stick to types, and only cast to void * when you need to.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • What is wrong with your example snippet? Why _you can't access ..._? – Enlico Aug 21 '22 at 11:20
  • https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule just double pointers here. `void *` is not compatible with `VeryComplexStruct *`, because `void` and `VeryComplexStruct` are not compatible. – KamilCuk Aug 21 '22 at 11:22
0
reinterpret_cast<VeryComplexStruct **>(&p)

There is no guarantee that the alignment requirements of void* and VeryComplexStruct* are compatible. If they are not and &p is not suitably aligned for a VeryComplexStruct*, then the value resulting from this cast will be unspecified and using it in basically any way will cause undefined behavior.

Even if the alignment requirement is satisfied, the resulting pointer may not be used to access the object it points to. There is no VeryComplexStruct* object which is pointer-interconvertible with the void* object at the address &p. Therefore the result of the cast will still point to the void* object, not a VeryComplexStruct* object.

Generally it is an aliasing violation, causing undefined behavior, to access an object of one type through an lvalue (e.g. a derereferenced pointer) of another type with a few specific exceptions, none of which apply here.

(There isn't even any guarantee that void* and VeryComplexStruct* have the same size and representation, although that is practically generally the case.)


(static_cast<VeryComplexStruct *>(p))->test();

Assuming the function has not modified p, this is trying to call a non-static member function on a null pointer, causing undefined behavior.

If the function did modify p in some way, which is legal basically only by first casting the argument back to void**, then it depends on what the function did do with p. The line is valid if p was assigned a pointer to a VeryComplexStruct* object or some object which is pointer-interconvertible with such. Otherwise the member function call is again going to have undefined behavior.


This may happen if we use C libraries in C++. Any idea?

It causes undefined behavior in the same way in C, although the object model and terminology used there is different. In that case the problem would be that void* and VeryComplexStruct* are not compatible types, so accessing the void* object through a pointer to VeryComplexStruct* would again be an aliasing violation. So if something like this is used in a C library, it already relies on undefined behavior.

user17732522
  • 53,019
  • 2
  • 56
  • 105