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();
这种设计最大的缺点是CarDealer
class必须拥有*cars
容器并管理它的存在,即它必须关心它[=75]的指针=] 还活着。由于 C++ does not support covariance on them 不能 return 智能指针这一事实使这变得困难。此外,如果 cars_begin
和 cars_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";
使用下一个 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();
这种设计最大的缺点是CarDealer
class必须拥有*cars
容器并管理它的存在,即它必须关心它[=75]的指针=] 还活着。由于 C++ does not support covariance on them 不能 return 智能指针这一事实使这变得困难。此外,如果 cars_begin
和 cars_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";