3

在以下问题中,其中一个答案表明对象的动态类型不能改变:被引用对象的动态类型何时可以改变?

但是,我从 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 指针很便宜,并且主要是因为纯粹的兴趣而询问。是什么禁用了这种编译器优化?

4

1 回答 1

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

为什么?因为在主编译器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。编译器会怎么做?你的猜测和我的一样好。

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

于 2019-06-16T20:54:08.783 回答