virtual table 创建线程安全吗?

Is virtual table creation thread safe?

请让我首先说明,我知道从 constructor/destructor 中调用虚函数是一种不好的做法。 然而,这样做的行为,尽管可能会混淆或不符合用户的期望,但仍然是明确定义的。

struct Base
{
    Base()
    {
        Foo();
    }
    virtual ~Base() = default;
    virtual void Foo() const
    {
        std::cout << "Base" << std::endl;
    }
};

struct Derived : public Base
{
    virtual void Foo() const
    {
        std::cout << "Derived" << std::endl;
    }
};

int main(int argc, char** argv) 
{
    Base base;
    Derived derived;
    return 0;
}

Output:
Base
Base

现在,回到我真正的问题。如果用户从不同线程的构造函数中调用虚函数,会发生什么情况。有竞争条件吗?它是未定义的吗? 或者换句话说。由编译器设置 vtable,线程安全吗?

示例:

struct Base
{
    Base() :
        future_(std::async(std::launch::async, [this] { Foo(); }))
    {
    }
    virtual ~Base() = default;

    virtual void Foo() const
    {
        std::cout << "Base" << std::endl;
    }

    std::future<void> future_;
};

struct Derived : public Base
{
    virtual void Foo() const
    {
        std::cout << "Derived" << std::endl;
    }
};

int main(int argc, char** argv) 
{
    Base base;
    Derived derived;
    return 0;
}

Output:
?

发件人:https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors

You can call a virtual function in a constructor, but be careful. It may not do what you expect. In a constructor, the virtual call mechanism is disabled because overriding from derived classes hasn’t yet happened. Objects are constructed from the base up, “base before derived”.

如果在您的异步函数被调用时 "construction phase" 尚未完成,它将调用调用对象的函数。

Is setting the vtable by the compiler, thread safe?

根据我的理解,它不是线程安全的,但除了分配器和初始化程序之外,没有人应该修改该内存位置

是的。

严格来说,没有。您 可能 通过一些努力,构造一个至少在形式上不是线程安全的恶意示例。不过,它在实践中仍然是线程安全的。

除了试图完全恶意地令人讨厌,特别是在您的问题的上下文中,它肯定是 是的

一个对象要么根本不存在,要么正在构造,要么完全构造。唯一有点尴尬的状态是正在建造的状态。
有时不鼓励在部分构造的对象上从构造函数调用虚函数,但这样做是完全合法的,只要知道其中的含义即可。即同一个对象在不同的​​时间是不同的东西。

至于你的具体例子:你正在做的是,你调用一个函数,你为它捕获 this,它当时指的是一个类型 Base 的对象。当线程最终到达 运行 时,线程将使用什么类型的对象是毫无疑问的,因为它必须使用 this 的一个副本,仅此而已。

该标准并未准确定义事物如何与继承和 vtables 以及所有这些一起工作(但这仅仅是未指定的,而不是未定义的行为)。实际上,它通常是指向正在更新的静态结构的指针,并且在大多数体系结构(所有 合理的 体系结构,就此而言)上无论如何都是原子操作。
所以即使需要某种原子性,它在实践中无论如何都会存在。

虽然它甚至不需要。捕获恰好发生在一个线程中,即当前正在构造对象的线程,并且对象可能仅处于一种状态,而且毫无疑问它是什么。

我相信[class.base.init]/16:

Member functions (including virtual member functions) can be called for an object under construction. Similarly, an object under construction can be the operand of the typeid operator or of a dynamic_­cast. However, if these operations are performed in a ctor-initializer (or in a function called directly or indirectly from a ctor-initializer) before all the mem-initializers for base classes have completed, the program has undefined behavior.

应该回答这个问题。然而,它是有缺陷的。解决方法是

However, if these operations are performed in a ctor-initializer (or in a function called directly or indirectly from a ctor-initializer) before not after all the mem-initializers for base classes have completed, the program has undefined behavior.

目前,该段说只有当成员函数的调用发生在 mem-initializers for base 类 完成之前,行为才是未定义的,但是没有'涵盖你的情况:当调用既不发生在 base 类 初始化完成之前,也不是 base 类 初始化完成发生在调用之前。

首先摘录一些与此上下文相关的标准:

[defns.dynamic.type]

type of the most derived object to which the glvalue refers [Example: If a pointer p whose static type is "pointer to class B" is pointing to an object of class D, derived from B, the dynamic type of the expression *p is "D". References are treated similarly. — end example]

[intro.object] 6.7.2.1

[..] An object has a type. Some objects are polymorphic; the implementation generates information associated with each such object that makes it possible to determine that object's type during program execution.

[class.cdtor] 11.10.4.4

Member functions, including virtual functions, can be called during construction or destruction. 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. [..]

正如您所写,它清楚地定义了 constructor/destructor 中虚函数调用的工作方式 - 它们取决于对象的 动态类型 ,以及与对象关联的动态类型信息,以及该信息在执行过程中变化。您使用哪种指针指向 "look at the object" 无关紧要。考虑这个例子:

struct Base {
  Base() {
    print_type(this);
  }

  virtual ~Base() = default;

  static void print_type(Base* obj) {
      std::cout << "obj has type: " << typeid(*obj).name() << std::endl;
  }
};

struct Derived : public Base {
  Derived() {
    print_type(this);
  }
};

print_type 总是收到指向 Base 的指针,但是当您创建 Derived 的实例时,您将看到两行 - 一行带有 "Base" ,另一行带有 "Derived"。动态类型设置在构造函数的最开始,因此您可以调用虚函数作为成员初始化的一部分。

未指定如何何处存储此信息,但它与对象本身相关联。

[..] the implementation generates information associated with each such object [..]

为了更改动态类型,必须更新此信息。这可能是编译器引入的一些数据,但是对该数据的操作仍然被内存模型覆盖:

[intro.memory] 6.7.1.3

A memory location is either an object of scalar type or a maximal sequence of adjacent bit-fields all having nonzero width. [ Note: Various features of the language, such as references and virtual functions, might involve additional memory locations that are not accessible to programs but are managed by the implementation. — end note]

所以与对象关联的信息在某些内存位置中存储和更新。但那是数据竞争发生的情况:

[intro.races]

[..]
Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.
[..]
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other [..]

动态类型的更新不是原子的,并且由于没有其他同步会强制执行先发生顺序,因此这是一个数据竞争,因此是 UB。

即使更新是原子的,只要构造函数还没有完成,你仍然无法保证对象的状态,所以没有意义使其成为原子。


更新

从概念上讲,感觉 对象在构建和销毁过程中采用不同的类型。但是,@LanguageLawyer 向我指出,对象的 动态类型 (更准确地说是指代该对象的泛左值)对应于 most派生类型,并且此类型已明确定义并且不会更改。 [class.cdtor] 还包括有关此细节的提示:

[..] 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.

因此,即使虚函数调用和 typeid 运算符的行为被定义为好像对象具有不同的类型,但实际上并非如此。

也就是说,为了实现指定的行为,必须更改对象状态中的某些内容(或至少与该对象关联的某些信息)。正如 [intro.memory] 中指出的那样,这些额外的内存位置确实是内存模型的主题。所以我仍然坚持我的初步评估,即这是一场数据竞赛。