Consider the following declarations of a pair of related structs. The descendant class adds no member variables, and the only member function is a constructor that does noth
BEWARE! While this is almost certainly true in your compiler, this is not guaranteed by the standard to work.
At least add if (sizeof(Derived) != sizeof(Base)) logAndAbort("size mismatch between Derived and Base"); check.
In case you were wondering, the compilers for which this is safe are one to one in which the size doesn't change. There was something left behind in the standard that allows derived classes to be non-contiguous with base classes. In all cases where this happens, the size must grow (for obvious reasons).
Adding new data to the Descendent
class will break Descendent[]
's interchangeability with Base[]
. In order for some function to pretend an array of larger structures is an array of smaller but otherwise compatible structures, a new array would have to be prepared in which the extra bytes are sliced off, in which case it is impossible to define the behavior of the system. What happens if some pointers are sliced off? What happens if the state of these objects is supposed to change as part of the called procedure, and the actual objects to which they refer are not the originals?
Otherwise, if no slicing occurs and a Base*
to the Derived[]
was ++
ed, sizeof(Base)
would be added to its binary value, and it would no longer point to a Base*
. There is obviously no way to define the behavior of the system in that case either.
Knowing that, using this idiom is NOT safe, even if the standard and the president and God define it as working. Any addition to Descendent
breaks your code. Even if you add an assertion, there will be functions whose legitimacy depends on Base[]
being interchangeable with Descendent[]
. Whoever maintains your code will have to hunt down each of these cases and come up with an appropriate workaround. Factoring your program around this idiom to avoid these problems will probably not be worth the convenience.
While this particular example is safe on all modern platforms and compilers I am familiar with, it is not safe in general and it is an example of a bad code.
UPD. Both Base and Descendant are standard layout types. So it is a requirement of the standard that a pointer to Descendant can be correctly reinterpret_cast to a pointer to Base, that means no padding in front of the structure is allowed. But there is no any requirement in C++ standard for padding at the end of a structure, so it is compiler-dependent. There is also the standard proposal to explicitly mark this behavior as undefined. http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1504
Is behavior of this program defined? Does the answer depend on the body of foo, like whether it writes to the array or only reads from it?
I'm gonna hazard an answer saying that the program is well defined (as long as foo
is) even if it is written in another language (e.g. C).
If
sizeof(Derived)
is unequal tosizeof(Base)
, then behavior is undefined according to the answers to a previous question about a base pointer to an array of derived objects. Is there any chance the objects in this question will have differing sizes, though?
I don't think so. According to my reading of the standard(*) §9.2 clause 17
Two standard-layout struct (Clause 9) types are layout-compatible if they have the same number of non-static data members and corresponding non-static data members (in declaration order) have layout-compatible types (3.9).
§9 clauses 7 through 9 detail the requirements for layout-compability:
7 A standard-layout class is a class that:
has no non-static data members of type non-standard-layout class (or array of such types) or reference,
has no virtual functions (10.3) and no virtual base classes (10.1),
has the same access control (Clause 11) for all non-static data members,
has no non-standard-layout base classes,
either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and
has no base classes of the same type as the first non-static data member.
8 A standard-layout struct is a standard-layout class defined with the class-key
struct
or the class-keyclass
. A standard-layout union is a standard-layout class defined with the class-keyunion
.9 [ Note: Standard-layout classes are useful for communicating with code written in other programming languages. Their layout is specified in 9.2. — end note ]
Note especially the last clause (combined with §3.9) - according to my reading this is guaranteeing that as long as you're not adding too much "C++ stuff" (virtual functions etc. and thus violating the standard-layout requirement) your structs/classes will behave as C structs with added syntactical sugar.
Would anyone have doubted the legality if Base
didn't have a constructor? I don't think so as that pattern (deriving from a C structure adding a constructor/helper functions) is idiomatic.
I'm open to the possibility that I'm wrong and welcome additions/corrections.
(*) I'm actually looking at N3290 here, but the actual standard should be close enough.
If you declare an array of pointers to Base, then the code will run correctly. As a bonus, the new foo() will be safe to use with some future subclass of Base that has new data structures.
void foo(Base **array, unsigned len)
{
// Example code
for(unsigned i = 0; i < len; ++i)
{
Base *x = array[i];
std::cout << x->a << x->b;
}
}
void do_something()
{
Base *data[2];
data[0] = new Base(1, "a");
data[2] = new Descendent(2, "b");
foo(data, 2);
delete data[0];
delete data[1];
}