Consider the following code:
#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));
}
As expected, only a is printed. Well, a and two padding bytes that follow i in base class B.
Then imagine that we make B's member function bar virtual. That will make both classes polymorphic. In this configuration the program would output abc in both clang and gcc. Which means that they both calculate offsets based on polymorphic types and some runtime info. Which is nonsense, as far as I am concerned.
I also tried to add another derived class with different layout:
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));
In my case the output was a bizarre apB, which is not what I initialized those with. So it seems that no runtime info is used to calculate the offsets.
So, my question is pretty my staightforward:
- When iterating over an array of derived via a pointer to a base in a polymorphic context, which offset will be used according to the standard?
I sifted through the standard and found no mention of runtime info affecting the offsets.
[expr.add] doesn't really clarify the matter. Techically it says that the resulting pointer shall point at the element of an array.
It becomes really odd for me when I make the foo print:
#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));
}
It prints I AM DERIVED with correct char for the first iteration, and only one CERIVED for the second one, then fails with SIGSEGV.
I can reproduce it with both latest and gcc. The link to godbolt.
EDIT
Technically, the question is answered. If anybody is interested in why iterating over array of D via pointer to B with virtual functions worked (which was an initial concern of mine), here is my suggestion - with virtual functions the vtable pointer is added to the structs, which forces the structs to align on 8 byte boundary. This bloats B and D to 16 bytes, and C to 24 (in platform used in the examples). So iterating array of D via pointer to B works only because the class sizes are the same. But this is UB nonetheless.
void foo(B *arr, size_t size)can be used for an array of B's, not D's. It could also be used for a (pointer to) a single B or derived object, with size being irrelevant to this. "When iterating over an array of derived via a pointer to a base in a polymorphic context" There is no such legal operation. You need to come up with a different design to achieve your goal. Arrays are not polymorphic. One way would be to use arrays of pointers.