模板或抽象基类?

时间:2009-03-02 14:49:41

标签: c++ design-patterns templates abstract-class virtual-functions

如果我想使类适应,并且可以从外部选择不同的算法 - 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

你能评论这些可能性吗?

9 个答案:

答案 0 :(得分: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();
    }
}

如果您有很多政策,可以将它们组合到自己的结构中,然后传递一个,例如SpeedConfiguration收集AcceleratorBrakes等等。在我的项目中,我尝试保留大量无代码模板,允许将它们编译成自己的目标文件,而不需要在头文件中使用代码,但仍然允许多态(通过虚函数)。例如,您可能希望保留非模板代码可能在基类中多次调用的公共数据和函数:

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,format flags和stuff这样的特性,而std::basic_ios则是继承它的类模板。这还通过共享类模板的所有实例化所共有的代码来减少代码膨胀。

私有继承?

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

阅读Herb Sutter的Uses and abuses of Inheritance

答案 1 :(得分:32)

经验法则是:

1)如果在编译时选择具体类型,则更喜欢模板。它会更安全(编译时错误与运行时错误)并且可能更好地优化。 2)如果选择是在运行时进行的(即由于用户的操作),则实际上没有选择 - 使用继承和虚函数。

答案 2 :(得分:6)

其他选择:

  1. 使用Visitor Pattern(让您的班级使用外部代码)。
  2. 将类的某些部分外部化,例如通过迭代器,通用迭代器代码可以对它们起作用。如果您的对象是其他对象的容器,则此方法效果最佳。
  3. 另请参阅Strategy Pattern(里面有c ++示例)

答案 3 :(得分:5)

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

答案 4 :(得分:4)

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

  

来自Java,我很自然   倾向于总是使用版本1,但是   模板版本似乎是   经常优选,例如在STL代码?如果   这是真的,是因为   记忆效率等(没有继承,   没有虚函数调用)?

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

另一个因素是它允许更多的灵活性。为什么所有Brake实现都必须从同一个基类继承?基类实际上有什么东西要带到桌面上吗?如果我写一个暴露预期成员函数的类,那还不足以作为制动器吗?通常,显式地使用接口或抽象基类会使代码超出必要的限制。

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

答案 5 :(得分:3)

this answer或多或少是正确的。当你想要在编译时参数化的东西 - 你应该更喜欢模板。当你想要在运行时参数化的东西时,你应该更喜欢被覆盖的虚拟函数。

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

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

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

答案 6 :(得分:2)

抽象基类具有虚拟调用的开销,但它的优点是所有派生类都是基类。使用模板时不是这样 - Car&lt; Brake&gt;和Car&lt; BrakeWithABS&gt;彼此无关,你必须要么动态广播并检查null,要么为所有处理Car的代码都有模板。

答案 7 :(得分:0)

如果您希望一次支持不同的Break类及其层次结构,请使用interface。

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

你在编译时不知道这些信息。

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

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

答案 8 :(得分:0)

就个人而言,我总是喜欢在模板上使用Interfaces,原因如下:

  1. 模板编译和链接错误有时是神秘的
  2. 很难调试基于模板的代码(至少在visual studio IDE中)
  3. 模板可以让您的二进制文件更大。
  4. 模板要求您将所有代码放在头文件中,这会使模板类更难理解。
  5. 新手程序员很难维护模板。
  6. 我只在虚拟表创建某种开销时才使用模板。

    当然,这只是我的自我意见。