Given the following example of CRTP:
template <typename T>
int foo(T* const)
{
return 0;
}
template <typename Derived>
struct Base
{
Base() : bar(foo(static_cast<Derived*>(this)) {};
int bar;
};
struct Derived1 : Base<Derived1> {};
Is the conversion of this
to Derived*
here valid? I seem to recall that it may not be, but can find no concrete evidence of this now.
The "natural" type of this
at this stage is Base* const
, and there are certainly some occasions in which even statically casting the this
pointer during initialisation is not okay, such as upcasting before base construction has completed (12.7/3).
@DeadMG says:
there's an explicit exception in the Standard w.r.t. obtaining this in the initializer list. it's for passing pointers to yourself to subobjects.
12.6.2/12 does say:
[ Note: Because the mem-initializer are evaluated in the scope of the constructor, the this pointer can be used in the expression-list of a mem-initializer to refer to the object being initialized. —end note ]
... though this is not sufficient to say that the conversion to Derived*
is valid.
My intuition is that, during this phase of the object's initialisation, this
does not point to an instance of Derived
and, as such, even just having a pointer to it with type Derived*
is, strictly speaking, UB. That's because it's neither a valid pointer nor a null pointer.
(This potentially has practical ramifications for approaches like this, though in that answer and my example above the whole thing could be side-stepped by simply writing static_cast<Derived*>(0)
instead.)
I think that it is UB.
As Mike said:
At that point, storage has been allocated for the complete object, but only the base subobject has been initialised. Therefore, you can only use the rest of an object in the "limited ways" described in C++11.
and it's my interpretation that this is not one of those ways.
More formally:
[C++11: 3.8/1]:
The lifetime of an object is a runtime property of the object. An object is said to have non-trivial initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor. [ Note: initialization by a trivial copy/move constructor is non-trivial initialization. — end note ] The lifetime of an object of typeT
begins when:
- storage with the proper alignment and size for type
T
is obtained, and- if the object has non-trivial initialization, its initialization is complete.
and:
[C++11: 3.8/5]:
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of typevoid*
, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:
- the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
- the pointer is used to access a non-static data member or call a non-static member function of the object, or
- the pointer is implicitly converted (4.10) to a pointer to a base class type, or
- the pointer is used as the operand of a
static_cast
(5.2.9) (except when the conversion is tovoid*
, or tovoid*
and subsequently tochar*
, orunsigned char*
), or- the pointer is used as the operand of a
dynamic_cast
[..]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With