英文:
Why do I get a wrong pointer to a base class with a virtual constructor on C++?
问题
当声明一个虚拟析构函数时,类型转换后的指针 sa_in6
不再指向继承的 sockaddr_storage
结构的开头,因此导致无法正确访问 AF_INET6 数据。这是因为虚拟函数表(vtable)的存在,它会影响派生类对象的内存布局。
虚拟函数表是用来实现多态性的机制,它包含了指向派生类虚拟函数的指针。当你声明一个虚拟析构函数时,编译器会在对象的内存布局中添加一个指向虚拟函数表的指针。这就是为什么使用虚拟析构函数后,ss
和 sa_in6
指向不同的内存位置的原因。
在你的代码中,ss
仍然指向 sockaddr_storage
结构的开头,因为它是一个对象,没有受到虚拟函数表的影响。但是,sa_in6
是一个指针,它受到虚拟函数表的影响,因此它指向的位置不正确。
要解决这个问题,你可以使用非虚拟析构函数的结构 Ssockaddr_storage1
来确保 sa_in6
指向正确的位置。虚拟析构函数通常只在需要多态性的类中使用,而不适合用于简单的数据结构扩展。
希望这个解释对你有所帮助。如果你有其他问题,可以继续提出。
英文:
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?
答案1
得分: 9
这是因为这是未定义行为。
sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
C++不同于C(显然)。在C中,类似的强制转换是有效的,但在C++中却是未定义行为,因为C++的类型检查更加严格。ss
是Ssockaddr_storage2
,与sockaddr_in6
没有任何关系。
sockaddr_storage *sas= &ss;
此转换不需要强制转换,因为sockaddr_storage
只是Ssockaddr_storage2
的超类。
现在,你可以将这个POD C结构视为sockaddr_in6
:
sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;
现在,解释了这一点,你观察到的行为原因是一旦引入虚拟继承,大多数C++实现会向具有虚拟继承的对象添加额外的隐藏指针,以处理所有实现细节。这个隐藏指针通常放置在类实例的开头,而这个无效的强制转换现在指向的不再是sockaddr_storage
,因为它现在在类中的位置更靠后,而是指向虚拟表指针。这将导致不符合预期的结果。
英文:
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.
答案2
得分: 1
以下是您要翻译的部分:
"其他答案(由Ingo和Sam提供)适用于C++,但为了清楚地记录强制类型转换之间的差异,您可以在C++中使用命名强制类型转换:
sockaddr_in6* sa_in6 = reinterpret_cast<sockaddr_in6*>(static_cast<::sockaddr_storage*>(&ss));
这是C++核心指南建议的做法:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-cast"
英文:
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
答案3
得分: 0
As Sam Varshavchik 指出在他的回答中,问题是使用C编程语言进行的通常类型转换。这在C++上具有未定义的行为。
sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
为了总结Sam的解释,我只需要告诉编译器使用引用地址&ss
作为指针:
myns::Ssockaddr_storage2 ss;
sockaddr_in6* sa_in6 = (sockaddr_in6*)(::sockaddr_storage*)&ss;
std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
英文:
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";
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论