Reputation: 2989
I have some code. It doesn't work.
At first, you will look at this sample code snippet and think "WHY?" but trust me: there is a reason.
Here's the code:
class LinkedListNode
// blaa
{
public:
LinkedListNode ( void* p )
{
// blaa
}
} ;
template <typename T>
class InheritAndLinkList
: public virtual T
, public LinkedListNode
{
public:
InheritAndLinkList ()
: LinkedListNode ( static_cast<void*>(static_cast<T*>(this)) ) // an exception occurs here when ..... (scroll down)
{ }
} ;
template <typename T>
class Implements
: public virtual InheritAndLinkList<T>
{ } ;
class A
{
public:
virtual void goA () =0 ;
} ;
class B
: public Implements<A>
{
public:
virtual void goB () =0 ;
} ;
class MyClass
: public Implements<B>
{
public:
virtual void goA ()
{
// blaa
}
virtual void goB ()
{
// blaa
}
} ;
int main ( ... )
{
MyClass * p = new MyClass () ; // ..... This line executes
p->goA() ;
p->goB() ;
return 0 ;
}
The specific error is that, upon construction, the expression static_cast<T*>(this)
causes a segmentation fault....... when using the Intel C++ compiler. This has been working for years on many version of GCC, LLVM, MS Visual Studio, etc. And now ICPC makes it die.
I believe this is a perfectly valid thing to do. By the time this line gets called, T
has been constructed and should be valid to use... unless there is another weird thing in the C++ spec about that.
Putting the static_cast
in the constructor body (and changing its super to match) causes it to avoid this seg-fault.
So my question: where in the spec does it say this [static cast] is/isn't safe?
Upvotes: 3
Views: 638
Reputation: 8288
It may be a compiler bug.
I reduced and simplified the code example:
struct ctor_takes_int
{
// dummy parameter needed to put expression in a ctor-init-list of derived class
ctor_takes_int (int=0){ }
} ;
struct stupid_base
{
//int nevermind;
} ;
struct upcast_in_init_list;
/*
* volatile = anti optimisation :
* no value propagation possible on volatile variables
* no constant propagation
* no inlining of volatile pointer to function!
*/
int (*volatile upcast) (struct upcast_in_init_list *that);
struct upcast_in_init_list
: virtual stupid_base, ctor_takes_int
{
upcast_in_init_list ()
: ctor_takes_int (upcast(this))
{ }
} ;
/*
* volatile = anti optimisation
* no dead assignment removal
*/
stupid_base *volatile p;
// must be compiled out of line
int do_upcast (upcast_in_init_list *that) {
p = that;
return 0;
}
int main ()
{
upcast = &do_upcast;
new upcast_in_init_list() ;
return 0 ;
}
The program crashes on http://www.tutorialspoint.com/compile_cpp11_online.php
(Note the use of volatile
to prevent some optimisations, but it doesn't seem needed in practice. It's just more "robust", to have a "robust crash".)
If instead of calling a function, I do the up cast inside the ctor-init-list, with ((p = this,0))
, the program works. This means that the compilers knows how to perform the pointer conversion on this
inside the ctor-init-list of the object being constructor, but the common conversion code doesn't know how perform the conversion, as the derived object doesn't exist at that point (you can't use typeid
on it for example).
When you think about it in term of implementation, it's understandable: for non virtual base classes, the derived to base pointer conversion is a simple "if non null add a fixed offset" adjustment, but it involves something more complicated for virtual base classes, as they don't reside at a fixed offset, by definition (making a base class virtual is much like adding a level of indirection).
The essence of virtual items (virtual functions, virtual base classes) is a dependency on the dynamic (real) type of the object. Note that a class without virtual functions but with a virtual base class isn't "polymorphic" in C++ and doesn't support RTTI (dynamic_cast
and typeid
), but must still have some "virtual" runtime information, either vptr (vtable pointer), or some virtual base offset or pointer. In either case, the runtime information is initialized during construction.
When the body of a constructor is entered (immediately after {
), the object being constructed doesn't officially "exists" as its lifetime has not started: if the constructor body exits with an exception, the corresponding destructor will not be invoked. But the un-started lifetime still has all the attribute of a "virtual" object (= an object with virtual features, either functions or base classes). Virtual functions can be called virtually and the overrider in the current class will be invoked, typeid
will indicate the type of the object in construction, etc.
In practice, conversions to non virtual base classes always work, in all compilers, as no "virtual"/dynamic information is used, just like calling non virtual functions "works" (in practice) on un-constructed objects, even if it isn't legal.
Also, conversion of the expression (not value) this
in the initialization list works, as it is a special optimized case: the compiler knows the layout of the class and the (static) offset of all virtual in the complete constructor (the constructor used to construct complete objects, not base class subobjects). You can see that this
is special cased: using (that = this, p = that, 0)
(where that
is some upcast_in_init_list *
variable) in a ctor-init-list of a complete constructor doesn't work, as the special isn't recognized anymore.
Handling of this
is a base class constructor call (a constructor call that doesn't get to initialize virtual base classes) apparently works too, I don't know why.
Upvotes: 0
Reputation: 52591
For what it's worth, the code looks OK to me. I don't see anything controversial in this use of static_cast
- it's run-of-the-mill derived-to-base pointer conversion. Looks like a compiler bug to me.
If you insist on chapter and verse:
[expr.static.cast]/4 An expression
e
can be explicitly converted to a typeT
using astatic_cast
of the formstatic_cast<T>(e)
if the declarationT t(e);
is well-formed, for some invented temporary variablet
(8.5). The effect of such an explicit conversion is the same as performing the declaration and initialization and then using the temporary variable as the result of the conversion.
So we are looking at the validity of T t(this);
within the constructor of InheritAndLinkList<T>
- a direct-initialization:
[dcl.init]/17 ...
-- Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression. Standard conversions (Clause 4) will be used, if necessary, to convert the initializer expression to the cv-unqualified version of the destination type; no user-defined conversions are considered.
.
[conv.ptr]/3 A prvalue of type “pointer to cv
D
”, whereD
is a class type, can be converted to a prvalue of type “pointer to cvB
”, whereB
is a base class (Clause 10) ofD
. IfB
is an inaccessible (Clause 11) or ambiguous (10.2) base class ofD
, a program that necessitates this conversion is ill-formed. The result of the conversion is a pointer to the base class subobject of the derived class object.
EDIT
After vigorous discussion in comments, using this
from within constructor initializer list is not quite as straightforward - but your particular use is still legal, I believe.
[class.cdtor]/3 To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class
X
to a pointer (reference) to a direct or indirect base classB
ofX
, the construction ofX
and the construction of all of its direct or indirect bases that directly or indirectly derive fromB
shall have started and the destruction of these classes shall not have completed, otherwise the conversion results in undefined behavior... [Example:struct A { }; struct B : virtual A { }; struct C : B { }; struct D : virtual A { D(A*); }; struct X { X(A*); }; struct E : C, D, X { E() : D(this), // undefined: upcast from E* to A* // might use path E* ! D* ! A* // but D is not constructed // D((C*)this), // defined: // E* ! C* defined because E() has started // and C* ! A* defined because // C fully constructed X(this) { // defined: upon construction of X, // C/B/D/A sublattice is fully constructed } };
— end example ]
Your case resembles X(this)
in the example above, and is actually simpler than that because you only cast one step up in the hierarchy, so there are no intermediate classes to be concerned about.
Upvotes: 4