多态对象数组迭代:基类指针遍历派生类数组的标准偏移规则问询
多态类型数组通过基类指针遍历的C++标准问题
请考虑以下代码:
#include <iostream> struct B { char i; B(char i) : i(i) {}; void bar() {}; }; struct D : B { int y; D(char i, int y) : B(i), y(y) {}; }; void foo(B *arr, size_t size) { for(B *end = arr + size; arr < end; ++arr) { std::cout << arr->i << std::endl; } } int main() { D arr[3] = { {'a', 65}, {'b', 66}, {'c', 67} }; foo(arr, sizeof(arr) / sizeof(*arr)); }
正如预期,这段代码仅输出a——准确来说是a以及基类B中i之后的两个填充字节。
接着,若将B的成员函数bar改为虚函数,会使两个类都成为多态类型。在此配置下,clang和gcc都会输出abc,看起来像是基于多态类型和某些运行时信息计算偏移量,但这显然不合理。
我还尝试添加另一个布局不同的派生类:
struct C : B { long y; C(char i, long y) : B(i), y(y) {}; }; //... C rra[] = { {'a', 65}, {'b', 66}, {'c', 67} }; foo(rra, sizeof(rra) / sizeof(*rra));
测试中输出了奇怪的apB,和初始化值不符,这说明并没有使用运行时信息计算偏移量。
因此我的问题非常明确:
- 在多态场景下,通过基类指针遍历派生类对象数组时,C++标准规定应使用哪种偏移量?
我查阅了C++标准,未发现任何关于运行时信息影响偏移量的描述。[expr.add]条款并未明确这一点,仅指出结果指针必须指向数组的某个元素。
当修改foo函数的输出逻辑后,情况变得更奇怪:
#include <iostream> struct B { char i; B(char i) : i(i) {}; virtual void foo() { std::cout << "I AM BASE" << i << std::endl; }; }; struct D : B { int y; D(char i, int y) : B(i), y(y) {}; virtual void foo() { std::cout << "I AM DERIVED" << i << std::endl; }; }; struct C : B { long y; C(char i, long y) : B(i), y(y) {}; virtual void foo() { std::cout << "I AM CERIVED" << i << std::endl; }; }; void foo(B *arr, size_t size) { for(B *end = arr + size; arr < end; ++arr) { std::cout << arr->i << std::endl; arr->foo(); } } int main() { D arr[] = { {'a', 65}, {'d', 66}, {'c', 67} }; foo(arr, sizeof(arr) / sizeof(*arr)); C rra[] = { {'a', 70}, {'d', 66}, {'c', 67} }; foo(rra, sizeof(rra) / sizeof(*rra)); }
测试中,第一次迭代会正确输出I AM DERIVED及对应的字符,而第二次仅输出一个CERIVED后,程序因SIGSEGV崩溃。该问题可在最新版本的clang和gcc中复现。
编辑补充
技术层面上问题已得到解释。若好奇为何带虚函数时,通过基类指针遍历D对象数组能正常工作(这是我最初的疑问),我的推测是:虚函数会为结构体添加虚表指针,迫使结构体按8字节边界对齐。这使得B和D的大小变为16字节,C变为24字节(基于示例使用的平台)。因此遍历D数组能正常工作只是巧合——类大小相同,但这仍然是未定义行为(UB)。
内容的提问来源于stack exchange,提问作者tntnkn
相关产品推荐
相关产品推荐

