C++ 类 上的继承包含另一个 Derived 类

Inheritance on C++ Classes that contain another Derived classes

使用下一个 C++ classes 作为问题的简化:

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

规则是:昂贵的汽车仅在 ExpensiveCarsRetailers 中出售(与廉价汽车类似)。便宜的车没有涡轮,贵的车不卖二手。

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

API(returning vector of Car* and 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();
}

我确信我遗漏了一些在这种情况下有帮助的设计模式,其中 class 继承树以其中一个继承树的抽象 class 需要的方式耦合到 return 另一个继承树上 classes 的向量(或集合、列表等)。我尽量避免动态转换。

我也考虑过将 CarRetailer 制作成模板 class,因此:

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

然后制作:

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

但我认为这行不通,因为例如 CarRetailer 也可以开始销售摩托车(与汽车结构相似)或自行车等(始终应用 Expensive/Cheap 模式)最终需要在 CarRetailer 中定义的模板 classes 的数量将是巨大的。

"coupling of classes that contains inherited classes as well" 的整个概念称为协方差。在维基百科上,可以找到对该主题的广泛评论:http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)。但是让我们回到实际的例子:写

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

你承诺的太多了,即 get_cars() 函数将 return 一个向量,你可以用 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!

解决方案是减少您在根 class 中所做的 "promises",returning 范围不能用 "extraneous" 对象更改。一个只读容器会很好,但遗憾的是,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();

这种设计最大的缺点是CarDealerclass必须拥有*cars容器并管理它的存在,即它必须关心它[=75]的指针=] 还活着。由于 C++ does not support covariance on them 不能 return 智能指针这一事实使这变得困难。此外,如果 cars_begincars_end 函数应该在重复调用时生成新集合,那么最终可能不得不维护一个 vector<Car>* 的容器。因此,您可以根据具体用例权衡我的建议的优缺点。

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

编辑

如果我很理解为什么你觉得模板化的Dealer不符合你的需求,我觉得这个建议应该更合适:

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

这个系统最大的好处就是你甚至不用提前规划接口,每个class都能实现你需要的方法。因此,如果将来您添加一个 CarMuseum,您可以决定实现 get_cars() 以使 print_car_table(T) 使用它,但您可以自由地不创建任何其他方法。使用继承,您将被迫实现在基接口中声明的所有功能,或者创建许多零散的接口 (class CheapDealer : public HasACarList, public HasAPriceList, /* yuck ...*/)。 .

这种模板化设计的缺点是经销商 class 不是亲戚的结果。这意味着你不能创建 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";