【问题标题】:Changing dynamic type of an object in C++在 C++ 中更改对象的动态类型
【发布时间】:2019-10-30 12:39:13
【问题描述】:

在以下问题中,其中一个答案表明对象的动态类型无法更改:When may the dynamic type of a referred to object change?

但是,我从 CPPCon 或其他会议上的一些演讲者那里听说这不是真的。

事实上,这似乎并不正确,因为 GCC 和 Clang 在以下示例的每次循环迭代中都会重新读取 vtable 指针:

class A {
public:
    virtual int GetVal() const = 0;
};

int f(const A& a){
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        // re-reads vtable pointer for every new call to GetVal
        sum += a.GetVal();
    }
    return sum;
}

https://godbolt.org/z/MA1v8I

但是,如果添加以下内容:

class B final : public A {
public:
    int GetVal() const override {
        return 1;
    }
};

int g(const B& b){
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        sum += b.GetVal();
    }
    return sum;
}

然后函数g被简化为return 10;,这确实是意料之中的,因为final。它还表明,动态可能发生变化的唯一可能位置是 GetVal 内部。

我知道重新读取 vtable 指针很便宜,并且主要是因为纯粹的兴趣而询问。是什么禁用了此类编译器优化?

【问题讨论】:

  • C++ 中的对象没有“动态类型”——它们的类型在编译时是固定的。您可以通过指针或引用来引用对象,就好像它们是不同类型的一样,但对象的实际类型没有改变。
  • @NeilButterworth “如果某个左值表达式引用了多态对象,则其最衍生对象的类型称为动态类型。” en.cppreference.com/w/cpp/language/type
  • 因为访问是通过指针。
  • I've heard that it is not true from some speaker on CPPCon 添加相应演讲的链接会很有帮助,因为这样就可以告诉你这个人的意思。
  • 关于以上评论交流;该标准在这里的术语很草率。在定义部分中,“动态类型”只为泛左值定义,而不是对象。然而,该标准确实在几个地方使用了未定义的术语“对象的动态类型”。其中一些用法可能意味着“glvalue 的动态类型”,但有些则不能。在其他情况下,也许它们的意思是“对象是其子对象的最派生对象”。

标签: c++ compiler-optimization lifetime virtual-functions vptr


【解决方案1】:

您不能更改对象的类型。您可以销毁对象并在同一内存中创建新的东西 - 这与“更改”对象类型最接近。这也是为什么对于某些代码编译器实际上会重新读取 vtable 的原因。但是检查这个https://godbolt.org/z/Hmq_5Y - vtable 只读一次。一般来说 - 不能改变类型,但可以从灰烬中摧毁和创造。

免责声明:请,请不要做这样的事情。这是一个糟糕的想法,混乱,任何人都难以理解,编译器可能对它的理解略有不同,一切都会变得很糟糕。如果你问这样的问题,你肯定不想在实践中实现它们。提出您真正的问题,我们会解决它。

编辑: 这不会飞:

#include <iostream>

class A {
public:
    virtual int GetVal() const = 0;
};

class C final : public A {
public:
    int GetVal() const override {
        return 0;
    }
};

class B final : public A {
public:
    int GetVal() const override {
        const void* cptr = static_cast<const void*>(this);
        this->~B();
        void* ptr = const_cast<void*>(cptr);
        new (ptr) C();
        return 1;
    }
};

int main () {
    B b;
    int sum = 0;
    for (int i = 0; i < 10; ++i) {
        sum += b.GetVal();
    }
    std::cout << sum << "\n";
    return 0;
}

为什么?因为在主编译器中将B 视为最终并且编译器通过语言规则知道,它控制对象b 的生命周期。所以它优化了虚拟表调用。

此代码有效:

#include <iostream>

class A {
public:
    virtual ~A() = default;
    virtual int GetVal() const = 0;
};

class C final : public A {
public:
    int GetVal() const override {
        return 0;
    }
};

class B final : public A {
public:
    int GetVal() const override {
        return 1;
    }
};

static void call(A *q, bool change) {
    if (change) {
        q->~A();
        new (q) C();
    }
    std::cout << q->GetVal() << "\n";
}
int main () {
    B *b = new B();
    for (int i = 0; i < 10; ++i) {
        call(b, i == 5);
    }
    return 0;
}

我使用new 在堆上分配,而不是在堆栈上。这可以防止编译器承担b 的生命周期管理。这反过来意味着它不再可以假设b 的内容可能不会改变。请注意,尝试在 GetVal 方法中从灰烬中升起可能也不会顺利 - this 对象必须至少与调用 GetVal 一样长。编译器会怎么做?你的猜测和我的一样好。

一般来说,如果您编写代码,这让编译器将如何解释它留下任何疑问(换句话说,您进入“灰色区域”,您、编译器制造商、语言编写者和编译器本身可能会有不同的理解),您问麻烦。请不要那样做。询问我们,为什么您需要这样的功能,我们会告诉您,如何根据语言规则实现它,或者您如何解决缺少它的问题。

【讨论】:

  • 您能详细说明一下吗?例如,以下内容不起作用:godbolt.org/z/-1AMpY
  • 我很确定您的“有效”样本未定义。不仅B* 指向一个不是B 的对象(即使你只将它用作A* 并不能使它正常),你还假设多态指针转换总是微不足道的(多重继承或虚拟继承并非如此)。
猜你喜欢
  • 2019-03-20
  • 1970-01-01
  • 2016-04-15
  • 1970-01-01
  • 2017-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多