Why does this incorrect CRTP static_cast compile?

K

kfrank29.c

Hello Group!

The Wikipedia CRTP article:

http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

has a comment that confused me:

Pitfalls

One issue with CRTP is that the correct usage of the
pattern by derived classes cannot be checked at compile
time. For example, with the above example of Shape_CRTP,
if the definition of Circle were changed to:

class Circle: public Shape_CRTP<Square> {};

it would still compile with no errors. However, running
code that performs clone() on a Circle object will lead
to undefined behavior.

(By the way, Mark's thread, "enforce access to derived class
via pointer to base," got me thinking about CRTP and led me
to the Wikipedia article.)

I've analyzed why I think the Wikipedia example should compile,
but I'm really not sure I understand what's going on.

(For context and details, see the article.)

I've put together a stripped-down example that illustrates
the main points of my analysis.

The core issue is that code within the template CRTP base
class looks as if it's making a compile-time illegal
static_cast when instantiated incorrectly, and I'm trying
to figure out why it's compile-time legal.

(I am a little foggy on what, exactly, static_cast is and
in not allowed to do, and I find the language in the
standard difficult to follow.)

Here is my example, followed by my analysis:


==========


class B { // base class for example
};

template <typename T> class CR : public B { // template class for CRTP
public:
void f() { // doesn't matter if this is virtual
// why does this compile when instantiated with F?
T *pt = static_cast<T*>(this);
}
};

class E : public CR<E> { // normal CRTP idiom
};

class F : public CR<E> { // error -- should derive from CR<F>
};

class X : public B { // to illustrate bad static_cast without CRTP
};

class Y : public B { // to illustrate bad static_cast without CRTP
};

int main (int argc, char *argv[]) {
E e;
e.f(); // okay -- static cast from base* to derived*

F f;
f.f(); // why doesn't this cause bad static_cast to be instantiated?

X x;

// error: invalid static_cast from type 'X*' to type 'Y*'
// Y *py = static_cast<Y*>(&x);

// compile-time legal static_cast from X* to B* to Y*
Y *py = static_cast<Y*>(static_cast<B*>(&x));
}


==========


(Note, CR derives from B only to mirror more closely the
Wikipedia example. I have the same analysis and confusion
without it.)

I think the point is that whether or not the function
CR<T>::f is virtual, the compile-time static type of
this in the code for f is CR<T>* for whatever T is when
CR is instantiated, even though at run time the type of
this is actually E* or F* (classes derived from CR).

The line e.f() causes the member function CR<E>::f() to be
instantiated. This causes the static cast from type CR<E>*
(the static type of this) to E* to be instantiated. This
should be fine, and is part of the standard CRTP idiom.

If the static type of this were E* for E derived from
CR<E>, then we would not even need the static cast, but
we do.

The line f.f() causes CR<F>::f() to be instantiated.
The way I analyze this, we still have a static_cast from
CR<E>* (still the static type of this) to E* -- still okay,
because E derives from CR<E> (even though we are dealing
with an F).

For an F, the dynamic type of this is F*. If the static
type of this were also F* for F derived from CR<E>, then
the static_cast wouldn't be enough to get from F* to E*.

I've tried to draw an analogy with using static_cast with
the complication of the CRTP stripped away.

At the end of the example code, X and Y both derive from
B. We can't static_cast from X* to Y* because neither does
X derive from Y nor vice versa. But we can do a two-step
static_cast from X* to B* to Y* (legal at compile time,
but undefined behavior at run time), because X and Y
derive from a common base class.

I believe that this is analogous to instantiating F::f().
this for F is of type F*, but the static type of this in
the code for f in CR<T> (when instantiated as CR<E>) is
CR<E>*. This is analogous to a static_cast of F* to its
pointer-to-base, CR<E>*. We then static_cast to E*, which
is compile-time legal (but undefined behavior at run time)
because E also derives from CR<E>.

To belabor the point, the two-step conversion:

F* --> CR<E>* --> E*

is analogous to the previous:

X* --> B* --> Y*

because E and F have CR<E> as their common base class,
and conversion from F* to E* is compile-time legal /
run-time undefined for the same reason that X* --> Y*
is.

There are a lot of steps in this chain of reasoning, and
I'm uncertain of the details. Any corrections or
clarifications would be very welcome.


Thanks.


K. Frank
 
Ö

Öö Tiib

The Wikipedia CRTP article:

http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern

has a comment that confused me:

Pitfalls

One issue with CRTP is that the correct usage of the
pattern by derived classes cannot be checked at compile
time. For example, with the above example of Shape_CRTP,
if the definition of Circle were changed to:

class Circle: public Shape_CRTP<Square> {};

it would still compile with no errors. However, running
code that performs clone() on a Circle object will lead
to undefined behavior.

The comment is correct, you get '*this' that is of type
'Circle const' cast into 'Square const&':

template <typename Derived>
class Shape_CRTP : public Shape {
public:
virtual Shape *clone() const {
return new Derived(static_cast<Derived const&>(*this));
}
};

It is illegal static_cast with undefined results. It is simple to get
rid of undefined behavior here by using dynamic_cast like that:

template <typename Derived>
class Shape_CRTP : public Shape {
public:
virtual Shape *clone() const {
return new Derived( dynamic_cast<Derived const&>( *this ) );
}
};


If that 'clone' is called for 'class Circle: public Shape_CRTP<Square> {};'
then std::bad_cast exception will be thrown runtime by the 'dynamic_cast'
and it is well-defined what will happen with it.

'static_cast' has been used in Wikipedia article because for some reason
people keep using cheaper but unsafe C++ features without any actual
performance problems.
 
K

kfrank29.c

Hi Öö!

The comment is correct, you get '*this' that is of type
'Circle const' cast into 'Square const&':

I admit that I am confused by all of this.

But I don't think your comment is correct, at least in
terms of what happens at compile time.

I think is this specific case where Circle has been incorrectly
instantiated as "class Circle: public Shape_CRTP<Square> {};"
the code is performing a static_cast that converts '*this',
of static type 'Shape_CRTP<Square> const' to a reference of
type 'Square const&', a (compile-time) legal use of static_cast,
because Square derives from Shape_CRTP<Square>.

If in fact, at compile time, "you get '*this' that is of type
'Circle const' cast into 'Square const&'", you should get a
compiler error, because neither Circle nor Square derives from
one another, so you can't use static_cast. The fact that they
derive from a common base class isn't enough.

At least that's how I understand it.
template <typename Derived>
class Shape_CRTP : public Shape {
public:
virtual Shape *clone() const {
return new Derived(static_cast<Derived const&>(*this));
}
};

It is illegal static_cast with undefined results.

I do agree that at run time it's undefined behavior (because
neither Circle nor Square derives from the other.)

But I believe that the Shape_CRTP code you quote is fully
legal, and it's only the instantiated code that becomes
become run-time illegal when you incorrectly derive Circle
from Shape_CRTP<Square>.

What's odd -- but I believe true -- is that the incorrect
instantiated code is compile-time legal, and only becomes
illegal / undefined at run time.
It is simple to get
rid of undefined behavior here by using dynamic_cast like that:

template <typename Derived>
class Shape_CRTP : public Shape {
public:
virtual Shape *clone() const {
return new Derived( dynamic_cast<Derived const&>( *this ) );
}
};

If that 'clone' is called for 'class Circle: public Shape_CRTP<Square> {};'
then std::bad_cast exception will be thrown runtime by the 'dynamic_cast'
and it is well-defined what will happen with it.

Agreed.

But, as a side note, as I understand it, CRTP is often used
in order to avoid the modest overhead of "polymorphic" base
classes (i.e., classes with virtual functions that participate
in RTTI).

In the Wikipedia example we are discussing, we have the virtual
clone and destructor, so RTTI / dynamic_cast is available.
But in other CRTP uses (e.g., the example code I gave), the
classes are not "polymorphic," so I don't think dynamic_cast
would be available.
'static_cast' has been used in Wikipedia article because for some reason
people keep using cheaper but unsafe C++ features without any actual
performance problems.

I agree that in the "Polymorphic copy construction"
Wikipedia example dynamic_cast would be better / safer.

But -- and please correct me if I am wrong -- in the
"Static polymorphism" example, the base class has no
virtual functions so RTTI, and hence dynamic_cast, can't
be used.

I am still quite uncertain about all of this.

I can talk myself into believing that I understand the
CRTP, but every time I look at it, I get confused again.


Again, thanks for your comments, and any additional
wisdom.


K. Frank
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,763
Messages
2,569,563
Members
45,039
Latest member
CasimiraVa

Latest Threads

Top