Consider the following classes:
template <class Derived>
class BaseCRTP {
private:
friend class LinkedList<Derived>;
Derived *next = nullptr;
public:
static LinkedList<Derived> instances;
BaseCRTP() {
instances.insert(static_cast<Derived *>(this));
}
virtual ~BaseCRTP() {
instances.remove(static_cast<Derived *>(this));
}
};
struct Derived : BaseCRTP<Derived> {
int i;
Derived(int i) : i(i) {}
};
int main() {
Derived d[] = {1, 2, 3, 4};
for (const Derived &el : Derived::instances)
std::cout << el.i << std::endl;
}
I know that it is undefined behavior to access the members of Derived in the BaseCRTP<Derived> constructor (or destructor), since the Derived constructor is executed after the BaseCRTP<Derived> constructor (and the other way around for the destructors).
My question is: is it undefined behavior to cast the this pointer to Derived * to store it in the linked list, without accessing any of Derived's members?
LinkedList::insert only accesses BaseCRTP::next.
When using -fsanitize=undefined, I do get a runtime error for the static_casts, but I don't know if it's valid or not:
instances.insert(static_cast<Derived *>(this));
crt-downcast.cpp:14:26: runtime error: downcast of address 0x7ffe03417970 which does not point to an object of type 'Derived'
0x7ffe03417970: note: object is of type 'BaseCRTP<Derived>'
82 7f 00 00 00 2d 93 29 f3 55 00 00 00 00 00 00 00 00 00 00 e8 7a 41 03 fe 7f 00 00 01 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'BaseCRTP<Derived>'
4
3
2
1
instances.remove(static_cast<Derived *>(this));
crt-downcast.cpp:17:26: runtime error: downcast of address 0x7ffe034179b8 which does not point to an object of type 'Derived'
0x7ffe034179b8: note: object is of type 'BaseCRTP<Derived>'
fe 7f 00 00 00 2d 93 29 f3 55 00 00 a0 79 41 03 fe 7f 00 00 04 00 00 00 f3 55 00 00 08 c0 eb 51
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'BaseCRTP<Derived>'
Additionally, here's a simplified version of the LinkedList class:
template <class Node>
class LinkedList {
private:
Node *first = nullptr;
public:
void insert(Node *node) {
node->next = this->first;
this->first = node;
}
void remove(Node *node) {
for (Node **it = &first; *it != nullptr; it = &(*it)->next) {
if (*it == node) {
*it = node->next;
break;
}
}
}
}
Up to now I didn't found a rule which says it is or is not UB, but I can explain why you see the observed behaviour.
In the C++ standard we have the following rule:
Member functions, including virtual functions (11.7.2), can be called during construction or destruction (11.10.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class.
Which basically says that calls to virtuals functions are allowed during construction. But those calls must not resolved to more derived classes.
This is achieved in clang by simply exchanging the VMT Pointer during construction.
This can be observed here: https://godbolt.org/z/3Gebvv (Or simply by analizing the generated assembly)
-fsanitize=undefined adds now some logic to find bugs / undefined behaviour. As an example they check if such a cast is valid. To do so they use the VMT which get's changed by the compiler.
In a non-constructor context an inappropriate vmt-pointer is a very good indication for undefined behaviour. But here could it be a bug in the sanitizer, too.
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