【问题标题】:Downcast to derived class in CRTP base class constructor: UB or not?在 CRTP 基类构造函数中向下转换为派生类:UB 与否?
【发布时间】:2020-07-18 12:36:08
【问题描述】:

考虑以下类:

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;
}

我知道在BaseCRTP&lt;Derived&gt; 构造函数(或析构函数)中访问Derived 的成员是未定义的行为,因为Derived 构造函数在BaseCRTP&lt;Derived&gt; 构造函数之后执行 (对于析构函数,反之亦然)。

我的问题是:将this 指针转换为Derived * 以将其存储在链接列表中是未定义的行为,不访问任何Derived 的成员吗? p>

LinkedList::insert 只访问BaseCRTP::next

使用-fsanitize=undefined 时,我确实收到static_casts 的运行时错误,但我不知道它是否有效:


    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>'

此外,这是LinkedList 类的简化版本:

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;
            }
        }
    }
}

【问题讨论】:

  • 您的LinkedList 是否不能访问Derived 的成员(例如:通过使用复制或移动构造函数)?
  • @UnholySheep 不,它只是保存一个指针,它不会复制或访问除 BaseCRTP::next 之外的任何其他内容。
  • @UnholySheep 我将LinkedList 类添加到我的问题中。
  • 根据 CWG1517,Derived 在其基类构造函数完成之前不会在构造中,但是......它如何影响不能static_cast 呢?
  • 能否请您添加您的“真实”链接列表。最后错过了;,不支持begin()/end()

标签: c++ language-lawyer undefined-behavior crtp downcast


【解决方案1】:

到目前为止,我还没有找到一条规则说明它是或不是 UB,但我可以解释为什么你会看到观察到的行为。

在 C++ 标准中,我们有以下规则:

可以在构造或销毁期间调用成员函数,包括虚函数 (11.7.2) (11.10.2)。当从构造函数或析构函数直接或间接调用虚函数时, 包括在类的非静态数据成员的构造或销毁过程中,以及对象 调用应用的是正在构造或销毁的对象(称为 x),调用的函数 是 构造函数或析构函数的类中的最终覆盖器,而不是在派生更多的类中覆盖它

这基本上是说在构造过程中允许调用虚函数。但是这些调用不能解析为更多的派生类。

这在 clang 中通过在构造过程中简单地交换 VMT 指针来实现。

这可以在这里观察到:https://godbolt.org/z/3Gebvv (或者简单地通过分析生成的程序集)

-fsanitize=undefined 现在添加了一些逻辑来查找错误/未定义的行为。例如,他们检查这样的演员表是否有效。为此,他们使用由编译器更改的 VMT。

在非构造函数上下文中,不适当的 vmt 指针是未定义行为的一个很好的指示。但是这也可能是消毒剂中的一个错误。

【讨论】:

  • 我强烈怀疑这种情况正在发生,因为 fsanitize 的实施正试图明智地采取行动。它不是做static_cast,而是尝试以动态转换的方式执行清理检查,并且由于构造函数vptr中发生的转换指向具有Base类型信息的VTable。如果删除虚拟方法,此 static_cast 不会导致 -fsanitize=undefined 出现问题。简单例子godbolt.org/z/c4rvMK
  • 是的,fsanitize 使用通过 VMT 提供的运行时类型信息来验证演员表。如果没有 VMT,则根本没有运行时类型信息,并且 fsanitize 无法检查太多...
  • fsanitize 并不是真正的“智能”,但它对多态类型进行了“更好”的检查——这会导致这里出现问题。
  • 感谢您的回答,尽管它并不能完全解释代码是否为 UB。我想知道您对此有何看法:我声称使用static_cast 向下转换只是dynamic_cast 的未检查(不安全)版本。在此代码中,dynamic_cast 将返回 null,因为尚未构造派生类。如果您认为有效的static_cast 也应该是成功的dynamic_cast 的逻辑,那么这里的static_cast 是无效的。你同意这个解释吗?无论如何,我仍然希望有一个关于 UB 的明确答案。
  • @JohnZwinck,我同意static_cast 用于向下转换的解释是dynamic_cast 的无效且未经检查(不安全)的版本。我认为这是因为-fsanitize 检查的实施而发生的。由于通过当前类 vptr 明显使用了 typeinfo,它在多态基类构造函数以外的函数中正确地通过 static_cast 捕获了无效向下转换。并且-fsanitize 应该通过static_cast 在多态基类中专门处理向下转换。
猜你喜欢
  • 2023-04-07
  • 2014-06-23
  • 2016-07-19
  • 1970-01-01
  • 2016-12-20
  • 1970-01-01
  • 2018-07-21
  • 2021-11-11
  • 1970-01-01
相关资源
最近更新 更多