仅仅为了代码重用而不必要地使用虚拟函数

Unnecessary usage of virtual functions just for code reuse

本文关键字:虚拟 函数 不必要 代码      更新时间:2023-10-16

我使用继承只是为了代码重用,我从不将类强制转换为基类。这是一个性能繁重的程序,所以我想避免使用virtual函数,但我不知道如何使用。考虑以下代码

class A{
public:
void print(){
std::cout << "Result of f() is: " << f() << std::endl;
}
virtual std::string f(){
return "A";
}
};
class B : public A{
public:
virtual std::string f(){
return "B";
}
};

对于函数f()不使用virtual函数,而在类B中不重新实现函数print(),这可能吗?我不在乎AB的基类,我只是不想再写f()。也许继承不是办法,也许模板可以用聪明的方式使用,但我不知道。

CRTP模式通常用于避免动态调度,因为它可以静态地确定为特定方法调用什么实现方法。

在您的示例中,AB都将从提供print()方法的单个基类继承。基类,我们称之为Print,是一个模板,其模板参数是一个提供f()的类。使这种模式获得"奇怪"绰号的转折点是,子类必须继承在子类上模板化的基类。这允许子类访问基类的print方法,但获得基类的一个版本,并通过扩展获得print的版本,该版本调用它们自己的f

下面是一个工作代码示例:

#include <iostream>
template<typename F>
class Print {
public:
void print() {
F& final = static_cast<F&>(*this);
std::cout << "Result of f() is: " << final.f() << std::endl;
}
};
class A: public Print<A> {
public:
std::string f(){
return "A";
}
};
class B: public Print<B> {
public:
std::string f(){
return "B";
}
};

int main() {
A a;
B b;
a.print();
b.print();
}

尽管print实现在AB之间被重用,但这里没有虚拟方法,也没有虚拟(运行时)调度或运行时检查。出现的一个强制转换是static_cast<>,其安全性已由编译器适当验证。

这是可能的,因为每次使用Print<F>,编译器都确切地知道F是什么。因此,已知Print<A>::print调用A::f,而Print<B>::print调用B::f,所有这些在编译时都是已知的。这使编译器能够像任何其他非虚拟方法调用一样内联和优化此类调用。

不利的一面是也没有继承。请注意,B不是从A派生的——如果是,则模式将不起作用,A::printB::print都将打印A,因为Print<A>就是这样输出的。更根本的是,不能在需要A*的地方传递B*——这是未定义的行为。事实上,AB不共享任何共同的超类,Parent<A>Parent<B>类是完全分离的。失去运行时调度(有缺点也有好处),转而启用静态调度,是静态多态性的一个基本折衷。

如果出于任何原因,您不想要动态绑定的"开销",可以省略virtual-关键字,这会使编译器使用静态绑定并禁用多态性。也就是说,编译器将在编译时仅根据变量的类型绑定实现,而不是在运行时。

老实说,我从来没有在不启用多态性的情况下为子类中的方法定义特定的实现。这样做通常表明没有特定的行为,即根本不应该在子类中重写该方法。

此外,将字符常量封装到字符串对象中的代码的性能成本已经远远高于动态绑定/vtable的"开销"。真的,重新思考两次,然后在进行此类"优化"之前衡量性能增长。

不管怎样,如果省略virtual,请查看代码的行为。注意,代码中的ptr->f()绑定到A::f,因为变量的类型是A*,尽管它指向类型为B:的对象

class A{
public:
void print(){
std::cout << "Result of f() is: " << f() << std::endl;
}
std::string f(){
return "A";
}
};
class B : public A{
public:
std::string f(){
return "B";
}
};
int main()
{
A a; cout << a.f(); // -> yields "A"
B b; cout << b.f(); // -> yields "B"
A* ptr = &b; cout << ptr->f(); // -> yields "A"; (virtual f, in contrast) would be "B"
return 0;
}

您可以使用模板来选择A和B的动态或非动态版本。这是一个相当棘手/丑陋的选项,但值得考虑。

#include <string>
template <bool Virt = false>
class A{
public:
std::string f(){
return "A";
}
};
template <>
class A<true> : A<false>{
public:
virtual std::string f(){
return A<false>::f();
}
};
template <bool Virt = false>
class B : public A<Virt>{
public:
std::string f(){
return "B";
}
};
std::string f1() { return B<>().f(); }
std::string f2(A<true> &a) { return a.f(); }
std::string f3() { B<true> b; return f2(b); }
#include <iostream>
int main(){
std::cout << f1() << 'n';
std::cout << f3() << 'n';
return(0);
}

值得注意的一点是,除了C++(预模板)早期做出的有争议的决定外,虚拟关键字在重写时应该是可选的,这是不可能的。