Thursday, June 20, 2013

Question? Why does this incorrect CRTP static_cast compile?

Newsgroups:    comp.lang.c++
    Date:    Wed, 24 Apr 2013 20:18:27 -0700 (PDT)
    Subject:    Why does this incorrect CRTP static_cast compile?
    From:    kfrank29.c@gmail.com

See all of the newgroup complang groups at pocketbinaries.com.

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.

No comments:

Post a Comment