39

如果我想让一个类具有适应性,并且可以从外部选择不同的算法——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++ FAQ

你能评论一下这些可能性吗?

4

9 回答 9

33

这取决于你的目标。如果您可以使用版本 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();
    }
}

如果你有很多策略,你可以将它们组合到它们自己的结构中,然后传递那个,例如作为SpeedConfigurationcollectionAccelerator等等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_iosthen 是继承它的类模板。这还通过共享类模板的所有实例所共有的代码来减少代码膨胀。

私人继承?

一般应避免私有继承。它很少有用,在大多数情况下,遏制是一个更好的主意。当大小非常重要时(例如基于策略的字符串类),通常情况相反:当从空策略类(仅包含函数)派生时,可以应用空基类优化。

阅读Herb Sutter的《继承的使用和滥用》 。

于 2009-03-02T15:31:08.207 回答
32

经验法则是:

1) 如果具体类型的选择是在编译时进行的,则首选模板。它会更安全(编译时错误与运行时错误)并且可能会得到更好的优化。2) 如果选择是在运行时做出的(即作为用户操作的结果),则实际上别无选择——使用继承和虚函数。

于 2009-03-02T15:10:18.693 回答
6

其他选项:

  1. 使用访问者模式(让外部代码在你的类上工作)。
  2. 将类的某些部分外部化,例如通过迭代器,基于通用迭代器的代码可以在它们上工作。如果您的对象是其他对象的容器,则此方法效果最佳。
  3. 另见策略模式(里面有 c++ 示例)
于 2009-03-02T14:58:12.887 回答
5

模板是一种让类使用您并不真正关心类型的变量的方法。继承是一种基于属性定义类的方法。它是“is-a”与“has-a”的问题。

于 2009-03-02T14:57:47.227 回答
4

您的大部分问题已经得到解答,但我想详细说明这一点:

来自 Java,我自然倾向于总是使用版本 1,但模板版本似乎更受欢迎,例如在 STL 代码中?如果这是真的,仅仅是因为内存效率等(没有继承,没有虚函数调用)吗?

这是其中的一部分。但另一个因素是增加的类型安全性。当您将 aBrakeWithABS视为 aBrake时,您会丢失类型信息。您不再知道该对象实际上是一个BrakeWithABS. 如果它是模板参数,则您有可用的确切类型,这在某些情况下可能使编译器能够执行更好的类型检查。或者它可能有助于确保调用正确的函数重载。(如果stopCar()将 Brake 对象传递给第二个函数,该函数可能具有单独的重载 for BrakeWithABS,如果您使用继承并且您BrakeWithABS已被强制转换为Brake.

另一个因素是它允许更大的灵活性。为什么所有的 Brake 实现都必须继承自同一个基类?基类实际上有什么要提出来的吗?如果我编写一个公开预期成员函数的类,那还不足以起到刹车的作用吗?通常,显式使用接口或抽象基类会过度限制您的代码。

(注意,我并不是说模板应该始终是首选解决方案。还有其他可能会影响这一点的问题,从编译速度到“我团队中的程序员熟悉什么”或只是“我喜欢什么”。有时,您需要运行时多态性,在这种情况下,模板解决方案根本不可能)

于 2009-03-02T16:00:15.720 回答
3

这个答案或多或少是正确的。当您想要在编译时参数化某些东西时 - 您应该更喜欢模板。当您希望在运行时参数化某些东西时,您应该更喜欢覆盖虚函数。

但是,使用模板并不妨碍您同时执行这两种操作(使模板版本更加灵活):

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()));

话虽如此,我可能不会为此使用模板......但这真的只是个人品味。

于 2009-03-02T15:54:23.440 回答
2

抽象基类具有虚拟调用的开销,但它的优点是所有派生类都是真正的基类。当您使用模板时并非如此—— Car<Brake> 和 Car<BrakeWithABS> 彼此无关,您必须进行 dynamic_cast 并检查 null 或为所有处理 Car 的代码提供模板。

于 2009-03-02T14:59:44.210 回答
0

如果您想同时支持不同的 Break 类及其层次结构,请使用接口。

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

而且您在编译时不知道这些信息。

如果您知道要使用哪个 Break 2b 是您指定不同 Car 类的正确选择。在这种情况下,刹车将是您的汽车“策略”,您可以设置默认值。

我不会用2a。相反,您可以将静态方法添加到 Break 并在没有实例的情况下调用它们。

于 2009-03-02T14:59:03.667 回答
0

就个人而言,由于以下几个原因,我总是更喜欢使用接口而不是模板:

  1. 模板编译和链接错误有时很神秘
  2. 很难调试基于模板的代码(至少在 Visual Studio IDE 中)
  3. 模板可以使您的二进制文件更大。
  4. 模板要求您将其所有代码放在头文件中,这使得模板类更难理解。
  5. 新手程序员很难维护模板。

我只在虚拟表产生某种开销时使用模板。

当然,这只是我个人的看法。

于 2009-04-10T10:42:38.573 回答