Downcast to derived class in CRTP base class constructor: UB or not? [duplicate]
Asked Answered
S

1

17

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;
            }
        }
    }
}
Steinke answered 6/4, 2020 at 13:32 Comment(6)
Doesn't your LinkedList access members of Derived (e.g.: by using a copy or move constructor)?Dunt
@Dunt no, it just saves a pointer, it doesn't copy or access anything else apart from BaseCRTP::next.Steinke
@Dunt I added the LinkedList class to my question.Steinke
According to CWG1517, Derived is not under construction until its base class constructor finishes, but... how does it affect that one can't static_cast to it?Mace
Could you please add your "real" Linked list. It misses ; at the end and it does not support begin() / end()Mcfarlane
@LanguageLawyer, it does not affect the static_cast but if you compile with -fsanitize=undefined then only it will throw a runtime error.Naamana
M
1

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.

Mcfarlane answered 28/11, 2020 at 20:44 Comment(5)
I strongly suspect that it is happening because the implementation of the fsanitize is trying to act smartly. instead of doing static_cast, it's trying to perform sanitize check the way a dynamic cast would have done and since casting happening in constructor vptr is pointing to VTable with type info of the Base. If you remove the virtual method, this static_cast does not cause problem with the -fsanitize=undefined. Simple example godbolt.org/z/c4rvMKNaamana
Yes, fsanitize uses the runtime type info provided via VMT to validate the cast. Without a VMT there is simply no runtime type info and fsanitize can't check to much...Mcfarlane
fsanitize is not really "smart", but it does a "better" check for polymorphic types - which causes the issue here.Mcfarlane
Thank you for this answer, even though it doesn't quite explain if the code is UB or not. I wonder what you think about this: I claim that downcasting with static_cast is just an unchecked (unsafe) version of dynamic_cast. In this code, dynamic_cast would return null because the derived class is not constructed yet. If you believe the logic that a valid static_cast should also be a successful dynamic_cast, then the static_cast here is not valid. Do you agree with this interpretation? In any case I'm still hoping for a definitive answer about UB or not.Whippersnapper
@JohnZwinck, I agree with the interpretation that static_cast used for downcasting is an invalid and unchecked(unsafe) version of dynamic_cast. I think this is happening just because of the implementation of the -fsanitize check. It catches the invalid downcasting via static_cast correctly in functions other than the polymorphic base class constructor because of obvious use of the typeinfo via current class vptr. And -fsanitize should specialize handling of downcasting via static_cast in polymorphic base class.Naamana

© 2022 - 2024 — McMap. All rights reserved.