Why do I get a wrong pointer to a base class with a virtual constructor on C++?

142 Views Asked by At

I want to derive a structure from the plain old trivial C structure ::sockaddr_storage so I can extend it with methods to handle its own data. I can type cast the derived structure as usual to get access to different socket addresses AF_INET6 or AF_INET stored in the structure. But I do not understand a problem when using a virtual destructor as shown below:

#include <netinet/in.h>
#include <iostream>

namespace myns {

struct Ssockaddr_storage1 : public ::sockaddr_storage {
    Ssockaddr_storage1() : ::sockaddr_storage() {}
    ~Ssockaddr_storage1() {}
};

// This is only different by using a virtual destructor.
struct Ssockaddr_storage2 : public ::sockaddr_storage {
    Ssockaddr_storage2() : ::sockaddr_storage() {}
    virtual ~Ssockaddr_storage2() {}
};

} // namespace myns

int main() {
    {
        myns::Ssockaddr_storage1 ss;

        std::cout << "Ssockaddr_storage1:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
    {
        myns::Ssockaddr_storage2 ss;

        std::cout << "\nSsockaddr_storage2 with virtual destructor:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
}

I compile it with:

~$ g++ -std=c++11 -Wall -Wpedantic -Wextra -Werror -Wuninitialized -Wsuggest-override -Wdeprecated example.cpp

The output is:

Ssockaddr_storage1:
ss_family   = 0
sin6_family = 0

Ssockaddr_storage2 with virtual destructor:
ss_family   = 0
sin6_family = 15752

As shown when using the reference ss to the inherited sockaddr_storage it always works. But when using the type casted pointer sa_in6 to access the AF_INET6 data it only works if not using a virtual destructor. If declaring a virtual destructor I get a random undefined address family number that differs from call to call. Obviously the pointer does not point to the right location.

Why the casted pointer sa_in6 does not point to the begin of the inherited sockaddr_storage structure when declaring a virtual destructor?

3

There are 3 best solutions below

2
Sam Varshavchik On BEST ANSWER

That's because this is undefined behavior.

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

C++ is not C (obviously). An analogous cast would be valid in C, but not in C++. In C++ this is undefined behavior, because C++'s type checking is more strict. ss is Ssockaddr_storage2, which is not related to sockaddr_in6, in any way.

sockaddr_storage *sas= &ss;

This conversion does not require a cast, since sockaddr_storage is just a superclass of Ssockaddr_storage2.

And now that you have a POD C struct, you can close your eyes and pretend that it's really a sockaddr_in6:

sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;

Now that we got this out of the way, the reason for your observed behavior is that once you introduce virtual inheritance most C++ implementations add an extra, hidden pointer to objects with virtual inheritance, to handle all the implementation details. This hidden pointer is typically placed at the beginning of the class's instances, and this invalid cast now ends up pointing not at sockaddr_storage, which now starts later in the class, but at the virtual table pointer. Hillarity ensues.

0
Ingo On

As Sam Varshavchik pointed out in his answer, the problem is the usual type cast done with the C programming language. This has undefined behavior on C++.

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

To summarize the explanation from Sam I only have to tell the compiler to use the reference address &ss as pointer:

myns::Ssockaddr_storage2 ss;
sockaddr_in6* sa_in6 = (sockaddr_in6*)(::sockaddr_storage*)&ss;
std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
1
Hans Olsson On

The other answers (by Ingo and Sam) work in C++, but to clearly document the difference between the casts you can use named casts in C++:

sockaddr_in6* sa_in6 = reinterpret_cast<sockaddr_in6*>(static_cast<::sockaddr_storage*>(&ss));

This is recommended by the C++ core guidelines https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-cast