包含另一个派生类的c++类的继承

Inheritance on C++ Classes that contain another Derived classes

本文关键字:继承 c++ 另一个 包含 派生      更新时间:2023-10-16

使用下一个c++类作为问题的简化:

struct Car
{
    virtual int get_price() = 0;
};
struct ExpensiveCar: public Car
{
    int get_price( ) {/*..*/ }
    void apply_turbo( ){/*..*/};
};
struct CheapCar: public Car
{
    int get_price( ) {/*..*/}
};
struct CarRetailer
{
    virtual std::vector<Car*> get_cars( ) = 0;
};

struct ExpensiveCarsRetailer : public CarRetailer
{
    virtual std::vector<Car*> get_cars( ) { /*..*/ }
    std::vector<ExpensiveCar*> get_cars_for_exhibitions( );
};
struct CheapCarsRetailer : public CarRetailer
{
    virtual std::vector<Car*> get_cars( ) { /*..*/ }
    std::vector<CheapCar*> get_second_hand_cars( );
};

规则是:昂贵的汽车只在昂贵的汽车零售商(类似于便宜的汽车)销售。便宜的车没有涡轮,昂贵的车不卖二手的。

我在这里面临的问题是包含继承类的类的耦合。因此,如果ExpensiveCarRetailer继承自CarRetailer,则需要实现它virtual std::vector<Car*> get_cars( )实际上是返回Car*的向量,然而,内部ExpensiveCarRetailer只创建了ExpensiveCar的对象。此外,get_cars_for_exhibitions()不包含在公共接口CarRetailer中,因此它可以返回std::vector<ExpensiveCar*>

API中的混合(Car*ExpensiveCar*的返回向量)非常难看,用户需要编写的代码也是如此,以便使用特定ExpesiveCarsRetailer的汽车列表的apply_turbo( )函数。

ExpensiveCarsRetailer ferrari;
std::vector<Car*> car = ferrari.get_cars();
ExpensiveCar* expensive_car;
for( int i = 0; i < car.size( ); ++i)
{
expensive_car = dynamic_cast<ExpensiveCar*>(car[i]);
expensive_car->apply_turbo();
}

我确信我在这种情况下错过了一些有帮助的设计模式,其中两个类继承树以一种继承树的抽象类需要返回另一个继承树上的类向量(或集合,列表等)的方式耦合在一起。我尽量避免动态强制转换。

我还考虑使CarRetailer模板化类,因此:

template<typename T>
    struct CarRetailer
    {
        virtual std::vector<T*> get_cars( ) = 0;
    };

然后加上:

struct ExpensiveCarRetailer: public CarRetailer<ExpensiveCar>
{
...
}

但是我不认为这会起作用,因为,例如,CarRetailer也可以开始销售摩托车(类似于汽车的结构)或自行车等(总是应用昂贵/廉价模式),最终需要在CarRetailer中定义的模板类的数量将是巨大的。

"包含继承类的类的耦合"的整个概念称为协方差。在维基百科上,人们可以找到关于这个主题的广泛评论:http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)。但是让我们回到实际的例子:通过写

struct CarRetailer
{
    virtual std::vector<Car*> get_cars( ) = 0;
};

你承诺太多,即get_cars()函数将返回一个向量,你可以用任何Car修改。这可能不是你的意思:

CarRetailer* ferrari = new ExpensiveCarsRetailer();
auto niceCars ferrari->get_cars();
niceCars.push_back(new Car{"Trabant"}); // you promised in the declaration that it was possible!

解决方案是减少你在根类中所做的"承诺",返回一个不能被"无关"对象改变的范围。一个只读容器会很好,但是,唉,c++还不够聪明,不能支持它的协方差:

struct CarRetailer
{
    virtual const std::vector<const Car*> get_cars( ) = 0;
};
struct ExpensiveCarsRetailer : public CarRetailer 
{
    const std::vector<const ExpensiveCar*> get_cars( ) = 0;
    // Alas, it won't override
};

但是范围(即指针对,希望c++ 17能提供更好的东西)将做:

struct CarRetailer
{
    virtual Car* const cars_begin( ) = 0;
    virtual Car* const cars_end( ) = 0;
};
struct ExpensiveCarsRetailer : public CarRetailer 
{
    ExpensiveCar* const cars_begin( ) override {return cars->begin();}
    ExpensiveCar* const cars_end( ) override {return cars->end();}
    private:
    vector<ExpensiveCar>* cars; 
};

(注意:我没有测试我的代码,所以请原谅可能的错误。这个概念应该很清楚,但是)

这个接口可能看起来比原来的那个更丑,但我的看法正好相反,因为它完美地集成了强大的<algorithm> c++库,并有助于以现代风格编写代码。下面是一个简单的例子:

any_of(dealer.cars_begin( ), dealer.cars_end( ),
       [](const auto& car) -> bool {return car.hasScratch();}
) ? complain() : congratulate();

这种设计的最大缺点是CarDealer类必须拥有*cars容器并管理它的存在,也就是说,它必须关心它返回的指针是否有效。由于c++不支持智能指针的协方差,因此不能返回智能指针,这使得这一点变得困难。此外,如果cars_begincars_end函数应该在重复调用时生成新的集合,那么最终可能不得不维护vector<Car>*的容器。因此,在您的具体用例中权衡我的建议的利弊取决于您。

就我个人而言,如果我有同样的问题,我会使用模板化类,完全避免继承:我认为这类问题很好地说明了为什么OOP有时会使事情复杂化,而不是简化它们。

编辑

如果我能很好地理解为什么你觉得模板经销商不能满足你的需求,我认为这个建议应该更合适。

struct ExpensiveCarsRetailer /* not derived, not templated */
{
    std::vector<ExpensiveCar> get_cars( ) { /*..*/ }
    // you can also return a vector of pointers or of unique_pointers, as you feel like.
};
struct CheapCarsRetailer /* not derived, not templated */
{
    std::vector<CheapCar> get_cars( );
};

使用模板化函数代替重载:

template <typename T> print_car_table(T dealer) {
    // This will work on both Expensive and Cheap dealers
    // Not even a hierarchy for the Car classes is needed:
    // they can be independent structs, like the Dealer ones
    auto cars = dealer.get_cars();
    for (const auto& car : cars) { std::cout << car.name() << "t" << car.color() << "n"; }
}
template <typename T> apply_turbo(T dealer) {
    // This will work if dealer is an ExpensiveDealer,
    // and fail on compile time if not, as wanted
    auto cars = dealer.get_cars();
    for (auto& car : cars) { car.apply_turbo(); }
}

这个系统的最大优点是你甚至不需要提前计划接口,每个类可以只实现你需要的方法。因此,如果将来您添加CarMuseum,您可以决定实现get_cars()以使print_car_table(T)与它一起工作,但您可以自由地不创建任何其他方法。使用继承,您将被迫实现在基接口中声明的所有函数,或者创建许多碎片接口(class CheapDealer : public HasACarList, public HasAPriceList, /* yuck ...*/)。

这种模板化设计的缺点是经销商类不是相关类的结果。这意味着您不能创建vector<Dealer>Dealer*(即使您使它们从一个很小的公共接口派生,您也不能通过指向该接口的指针调用get_cars())。

为完整起见,我将指出相反的动态解决方案:

struct Car {
    virtual int get_price() = 0;
    virtual void apply_turbo( ) = 0;
};
struct CheapCar: public Car
{
    int get_price( ) {/*..*/}
    void apply_turbo( ){throw OperationNonSupportedException();};
};

感觉不太符合c++的习惯,不是吗?我觉得它很不优雅,但我仍然认为这个设计应该在丢弃它之前进行评估。优点和缺点或多或少与模板化解决方案相反。

最后,我猜访问者模式或Scala(提供非常强大的协方差工具)可以为您的问题提供其他替代解决方案。但我没有这两方面的经验,所以我把它们留给其他人来说明。

为什么不像使用get_cars_for_exhibitions那样使用非虚拟函数来获取汽车呢?在示例中,您已经展示了调用者知道他们有一个ExpensiveCarsRetailer,所以他们不需要多态性。我假设你确实需要继承和多态性在其他情况下,但它会很容易从一个向量的ExpensiveCar指针转换到一个向量的Car指针实现CarRetailer接口:

template<typename T>
using Ptrs = std::vector<std::unique_ptr<T>>;
template<class T, class U>
Ptrs<U> upcast(Ptrs<T> input) {
  return Ptrs<U>(std::make_move_iterator(input.begin()),  
                 std::make_move_iterator(input.end()));
}
struct CarRetailer {
  virtual Ptrs<Car> getCars() = 0;
};
struct ExpensiveCarsRetailer : CarRetailer {
  Ptrs<ExpensiveCar> getCarsImpl() { ... }
  Ptrs<Car> getCars() override { return upcast<Car>(getCarsImpl()); }
};

如果需要ExpensiveCar指针的向量,可以调用非虚函数:

ExpensiveCarsRetailer ferrari;
auto expensive_cars = ferrari.getCarsImpl();
for (auto& expensive_car : expensive_cars)
  expensive_car->apply_turbo();

在多态上下文中,您可以调用虚函数:

CarRetailer* retailer = &ferrari;
auto cars = retailer->getCars();
for (auto& car : cars)
   std::cout << "Car price " << car->get_price() << "n";