-
03-07-2019 - |
题
如果我想让一个类具有适应性,并且可以从外部选择不同的算法——C++ 中最好的实现是什么?
我主要看到两种可能性:
- 使用抽象基类并传递具体对象
- 使用模板
这是一个在不同版本中实现的小例子:
版本1: 抽象基类
class Brake {
public: virtual void stopCar() = 0;
};
class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};
class Car {
Brake* _brake;
public:
Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};
版本 2a: 模板
template<class Brake>
class Car {
Brake brake;
public:
Car(){ brake.stopCar(); }
};
版本 2b: 模板和私有继承
template<class Brake>
class Car : private Brake {
using Brake::stopCar;
public:
Car(){ stopCar(); }
};
来自 Java,我自然倾向于始终使用版本 1,但模板版本似乎经常是首选,例如在STL代码中?如果这是真的,是否只是因为内存效率等(没有继承,没有虚函数调用)?
我意识到版本 2a 和 2b 之间没有太大区别,请参阅 C++ 常见问题解答.
您能评论一下这些可能性吗?
解决方案
这取决于您的目标。如果您满足以下条件,则可以使用版本 1:
- 打算更换汽车的制动器(在运行时)
- 打算将 Car 传递给非模板函数
我通常更喜欢使用运行时多态性的版本 1,因为它仍然很灵活,并且允许您让 Car 仍然具有相同的类型: Car<Opel>
是另一种类型 Car<Nissan>
. 。如果您的目标是在频繁使用刹车的同时获得出色的性能,我建议您使用模板化方法。顺便说一句,这称为基于策略的设计。您提供一个 制动政策. 。例如,因为您说您用 Java 编程,所以您可能对 C++ 还不太有经验。一种方法是:
template<typename Accelerator, typename Brakes>
class Car {
Accelerator accelerator;
Brakes brakes;
public:
void brake() {
brakes.brake();
}
}
如果您有很多策略,您可以将它们分组到自己的结构中,然后传递该策略,例如作为 SpeedConfiguration
收集 Accelerator
, Brakes
还有更多。在我的项目中,我尝试保持大量代码无模板,允许它们一次编译到自己的目标文件中,不需要在标头中包含它们的代码,但仍然允许多态性(通过虚拟函数)。例如,您可能希望保留非模板代码可能在基类中多次调用的通用数据和函数:
class VehicleBase {
protected:
std::string model;
std::string manufacturer;
// ...
public:
~VehicleBase() { }
virtual bool checkHealth() = 0;
};
template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
Accelerator accelerator;
Breaks breaks;
// ...
virtual bool checkHealth() { ... }
};
顺便说一句,这也是 C++ 流使用的方法: std::ios_base
包含不依赖于 char 类型或特征的标志和内容,例如 openmode、格式标志和内容,而 std::basic_ios
then 是继承它的类模板。这还通过共享类模板的所有实例化所共用的代码来减少代码膨胀。
私人继承?
一般来说,应该避免私有继承。它很少有用,并且在大多数情况下遏制是更好的主意。当大小确实至关重要时,情况恰恰相反(例如,基于策略的字符串类):当从空策略类(仅包含函数)派生时,可以应用空基类优化。
读 继承的使用和滥用 作者:赫伯·萨特。
其他提示
经验法则是:
1) 如果在编译时选择具体类型,则优先选择模板。它将更安全(编译时错误与运行时错误)并且可能更好地优化。2)如果选择是在运行时做出的(即作为用户操作的结果)确实别无选择 - 使用继承和虚函数。
模板是一种让类使用您并不真正关心其类型的变量的方法。继承是一种根据类的属性来定义类的方法。它的 “is-a”与“has-a” 问题。
你的大部分问题已经得到解答,但我想详细说明这一点:
来自Java,我自然倾向于始终使用版本1,但是模板版本似乎通常是首选的,例如在STL代码中?如果是这样,仅仅是因为内存效率等(没有继承,没有虚拟函数调用)?
这是其中的一部分。但另一个因素是增加的类型安全性。当您治疗一个 BrakeWithABS
作为一个 Brake
, ,你会丢失类型信息。你不再知道该对象实际上是一个 BrakeWithABS
. 。如果它是模板参数,则您可以使用确切的类型,这在某些情况下可能使编译器能够执行更好的类型检查。或者它可能有助于确保调用函数的正确重载。(如果 stopCar()
将 Brake 对象传递给第二个函数,该函数可能有一个单独的重载 BrakeWithABS
, ,如果您使用了继承,则不会被调用,并且您的 BrakeWithABS
已被投射到 Brake
.
另一个因素是它允许更大的灵活性。为什么所有 Brake 实现都必须继承自同一个基类?基类实际上有什么可以带来的吗?如果我编写一个公开预期成员函数的类,那么这还不足以充当刹车吗?通常,显式使用接口或抽象基类会对您的代码产生不必要的限制。
(请注意,我并不是说模板应该始终是首选解决方案。还有其他问题可能会影响这一点,从编译速度到“我团队中的程序员熟悉的内容”或只是“我喜欢的内容”。而有时候,你 需要 运行时多态性,在这种情况下模板解决方案根本不可能)
这个答案 或多或少是正确的。当您想要在编译时参数化某些内容时,您应该更喜欢模板。当您希望在运行时对某些内容进行参数化时,您应该更喜欢重写虚拟函数。
然而, ,使用模板并不妨碍您同时执行这两项操作(使模板版本更加灵活):
struct Brake {
virtual void stopCar() = 0;
};
struct BrakeChooser {
BrakeChooser(Brake *brake) : brake(brake) {}
void stopCar() { brake->stopCar(); }
Brake *brake;
};
template<class Brake>
struct Car
{
Car(Brake brake = Brake()) : brake(brake) {}
void slamTheBrakePedal() { brake.stopCar(); }
Brake brake;
};
// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));
话虽这么说,我可能不会为此使用模板......但这实际上只是个人品味。
抽象基类具有虚拟调用的开销,但它的优点是所有派生类都是真正的基类。当您使用模板时,情况并非如此 – Car<Brake> 和 Car<BrakeWithABS> 彼此无关,您必须进行 dynamic_cast 并检查 null 或为所有处理 Car 的代码提供模板。
如果您想同时支持不同的 Break 类及其层次结构,请使用接口。
Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )
而且你在编译时并不知道这些信息。
如果您知道要使用哪个 Break,则 2b 是您指定不同汽车类别的正确选择。在这种情况下,制动将是您的汽车“策略”,您可以设置默认策略。
我不会用2a。相反,您可以将静态方法添加到 Break 并在没有实例的情况下调用它们。
就我个人而言,由于以下几个原因,我总是更喜欢使用接口而不是模板:
- 模板编译和链接错误有时很神秘
- 调试基于模板的代码很困难(至少在 Visual Studio IDE 中)
- 模板可以使您的二进制文件更大。
- 模板要求您将所有代码放在头文件中,这使得模板类有点难以理解。
- 新手程序员很难维护模板。
我仅在虚拟表产生某种开销时才使用模板。
当然,这只是我个人的看法。