在派生 class 时将基数 class 转换为派生 class 仅添加非虚函数

Casting base class to derived class when derived class only adds non-virtual functions

假设我有这样的想法:

class Base {
public:
  Base(int value) : value_(value) {}
  int getValue() const { return value_; }
private:
  int value_;
};

class Derived : public Base {
public:
  // Derived only has non-virtual functions. No added data members.
  int getValueSquared() const { return value_ * value_; }
}

然后我执行以下操作:

Base* base = new Base(42);
Derived* derived = static_cast<Derived*>(base);
std::cout << derived->getValueSquared() << std::endl;

严格来说,这是UB。实际上,它工作得很好。

来自 Base 的实际数据成员(例如,int value_)必须位于相同的偏移处,无论对象是实际的 Base 还是实际的 Derived(否则,很好运气向上)。 getValueSquared() 不是 Derived 实例的实际内存占用的一部分,因此它不会从内存中的 Base 对象中“丢失”或未构造。

我知道 UB 是我不需要这样做的全部原因,但从逻辑上讲,它似乎总是有效。那么,为什么不呢?

我问这个问题是因为讨论它似乎是一个有趣的怪癖...不是因为我打算在生产中使用它。

实际上,大多数编译器会将非虚拟成员函数转换为带有隐藏 this 参数的静态函数。只要该函数不使用不属于基 class 的任何数据成员,它就可能工作。

UB 的问题是你无法预测它。昨天行得通的东西今天可能会失败,没有任何规律可言。编译器在如何解释技术上未定义的任何内容方面有很大的自由度,而寻找更好优化的竞赛意味着意想不到的变化可能会突然发生。墨菲定律指出,当您向最重要的老板或最大的客户演示软件时,这些变化将最为明显。

确实是UB。别名规则中断:

#include <iostream>

struct Base {
    int value = 0;
};

struct Derived1 : Base {
    void inc10() { value += 10; }
};

struct Derived2 : Base {
    void inc20() { value += 20; }
};

void doit(Derived1 *d1, Derived2 *d2) {
    std::cout << (&d1->value == &d2->value) << "\n";
    d1->inc10();
    d2->inc20();
    std::cout << d1->value << " " << d2->value << "\n";
}

int main() {
    Base b;
    doit(static_cast<Derived1*>(&b), static_cast<Derived2*>(&b));
    std::cout << b.value << "\n";
}

我的 GCC 11.2.0,当使用 g++ a.cpp -O2 -o a 编译时,打印

1
10 30
30

可以自由假设 Derived1*Derived2* 指向不同的对象,因此它优化了调用 d2->inc() 后对 d1->value 的额外读取,因为后者不能影响前者。

Clang 13.0.0 没有表现出这种行为,也很好。

Link to Godbolt