在C ++中更改对象的动态类型

Nik*_*nko 3 c++ virtual-functions lifetime compiler-optimization vptr

在以下问题中,一个答案建议对象的动态类型不能更改:所引用对象的动态类型何时可以更改?

但是,我听说在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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

然后将功能g简化为return 10;,这实际上是由于期望的final。这也表明,动态可能发生变化的唯一可能的地方是内部GetVal

我知道重新读取vtable指针很便宜,并且询问主要是出于纯粹的兴趣。是什么阻碍了这种编译器优化?

Rad*_*ski 5

您无法更改对象的类型。您可以销毁对象并在同一内存中创建新的东西-这与“更改”对象类型最接近。这也是为什么某些代码编译器实际上会重新读取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;
}
Run Code Online (Sandbox Code Playgroud)

为什么?因为在主编译器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;
}
Run Code Online (Sandbox Code Playgroud)

我曾经new在堆上分配,而不是在堆上分配。这样可以防止编译器假定对进行生命周期管理b。反过来,这意味着它不再可以假设的内容b可能不会更改。请注意,尝试从GetVal方法中的灰烬中进行筹集可能也无法顺利进行- this对象必须至少生存到调用to的时间GetVal。编译器将如何处理?你的猜测和我的一样。

通常,如果您编写代码,毫无疑问编译器将如何解释它(换句话说,您输入“灰色区域”,您,编译器制造商,语言作者和编译器本身可能会不同地理解),那么您就会遇到麻烦。拜托,不要那样做。问我们,为什么需要这样的功能,我们将告诉您,如何根据语言规则实现它,或者如何解决缺少它的问题。