在派生 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 没有表现出这种行为,也很好。
假设我有这样的想法:
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 没有表现出这种行为,也很好。