关于c ++:模板或抽象基类?

Template or abstract base class?

如果我想使类适应,并有可能从外部选择不同的算法-什么是最好的实现在C++中?

我主要看到两种可能性:

  • 使用抽象基类并在
  • 使用模板
  • 小精灵

    下面是在各种版本中实现的一个小示例:

    版本1:抽象基类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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:模板

    1
    2
    3
    4
    5
    6
    template<class Brake>
    class Car {
      Brake brake;
    public:
      Car(){ brake.stopCar(); }
    };

    版本2b:模板和私有继承

    1
    2
    3
    4
    5
    6
    template<class Brake>
    class Car : private Brake {
      using Brake::stopCar;
    public:
      Car(){ stopCar(); }
    };

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

    我意识到版本2A和2B之间没有很大区别,参见C++ FAQ。

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


    这取决于你的目标。如果您

    • 打算更换汽车制动器(运行时)
    • 打算将汽车传递给非模板函数
    • 小精灵

      我通常更喜欢使用运行时多态性的版本1,因为它仍然是灵活的,并且允许您让汽车仍然具有相同的类型:Car是另一种类型,而不是Car。如果您的目标在频繁使用刹车时表现出色,我建议您使用模板化方法。顺便说一下,这叫做基于策略的设计。你提供刹车政策。例如,因为你说你在爪哇编程,可能你还没有经验丰富的C++。一种方法是:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      template<typename Accelerator, typename Brakes>
      class Car {
          Accelerator accelerator;
          Brakes brakes;

      public:
          void brake() {
              brakes.brake();
          }
      }

      如果你有很多政策,你可以将它们组合成自己的结构,并通过这个结构,例如,作为一个SpeedConfiguration收集AcceleratorBrakes等等。在我的项目中,我试图保持大量的代码模板是免费的,允许它们一次编译成自己的对象文件,而不需要在头文件中使用它们的代码,但仍然允许多态性(通过虚拟函数)。例如,您可能希望保留非模板代码在基类中多次调用的公共数据和函数:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      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++流使用的方法:EDCOX1(5)包含不依赖于CHAR类型或特性的标志和属性,如OpenMODE、Frand FLAG和WITH,而EDCOX1(6)则是继承它的类模板。这还通过共享类模板的所有实例化通用的代码来减少代码膨胀。

      私人继承?

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

      阅读药草萨特对遗产的使用和滥用。


      经验法则是:

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


      其他选项:

    • 使用访问者模式(让外部代码在类上工作)。
    • 将类的某些部分外部化,例如通过迭代器,基于泛型迭代器的代码可以在这些部分上工作。如果您的对象是其他对象的容器,则此方法最有效。
    • 还可以看到策略模式(里面有C++例子)

    • 模板是一种让类使用一个你并不真正关心类型的变量的方法。继承是一种根据类的属性定义类的方法。这是一个"是"与"有"的问题。


      你的大部分问题都已经回答了,但我想详细说明一下:

      Coming from Java, I am naturally
      inclined to always use version 1, but
      the templates versions seem to be
      preferred often, e.g. in STL code? If
      that's true, is it just because of
      memory efficiency etc (no inheritance,
      no virtual function calls)?

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

      另一个因素是它允许更多的灵活性。为什么所有的制动实现都必须从同一个基类继承?基类真的有什么要带到桌面上的吗?如果我编写一个公开预期成员函数的类,那么它是否足够好,可以起到刹车的作用?通常,显式地使用接口或抽象基类比必要时更能约束代码。

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


      这个答案或多或少是正确的。当您希望在编译时参数化某些内容时,您应该更喜欢模板。当需要在运行时参数化某些内容时,您应该更喜欢重写虚拟函数。

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      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和car是相互不相关的,你要么动态地投射并检查空值,要么有处理car的所有代码的模板。


      就我个人而言,我总是喜欢使用界面而不是模板,因为以下几个原因:

    • 模板编译和链接错误有时很神秘
    • 很难调试基于模板的代码(至少在Visual Studio IDE中)
    • 模板可以使二进制文件更大。
    • 模板要求您将其所有代码放在头文件中,这使得模板类有点难以理解。
    • 新手程序员很难维护模板。
    • 我只在虚拟表产生某种开销时使用模板。

      当然,这只是我自己的看法。


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

      1
      2
      3
      Car( new Brake() )
      Car( new BrakeABC() )
      Car( new CoolBrake() )

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

      如果你知道你将使用哪个休息2b是正确的选择,你可以指定不同的汽车类别。在这种情况下,刹车将是你的汽车的"策略",你可以设置默认的一个。

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