现在如果需要实现 modifyShape 函数……这个函数在基类中应该是什么样子?
这个函数的外观是一个见仁见智的问题,但让我们通过以下方式解决这个问题:
- 识别函数的外观,并
- 基于一些“最佳做法”建议的替代外观。
C++ Core Guidelines 通常被称为“最佳实践”指南,它建议使用preferring concrete regular types。我们可以使用该指南来解决问题,并提供此功能和设计的外观。
首先,了解多态类型和多态行为之间存在差异。
多态类型是具有或继承至少一个虚函数的类型。这个shape 类和它的虚拟displayArea 成员函数就是这样一种多态类型。在 C++ 术语中,这些都是 T 类型,std:: is_polymorphic_v<T> 会为其返回 true。
就这个问题而言,多态类型与非多态类型的区别如下:
- 它们需要通过引用或指针来处理以避免切片。
- 它们不是自然规律的。 IE。它们不能被视为像
int 这样的基本 C++ 类型。
因此,以下代码不适用于您提供的设计,但指导是它确实有效:
auto myShape = shape{triangle{1.0, 2.0, 2.0}}; // triangle data is sliced off
myShape.displayArea(); // UB: invalid memory access in displayArea
myShape = circle(4); // now circle data is sliced off from myShape
myShape.displayArea(); // UB: also invalid memory access is displayArea
同时,更重要的是shape 的多态行为,因此形状可以是圆形或三角形。如您所见,使用多态类型是一种提供多态行为的方法,但它不是唯一的方法,并且存在您询问如何解决的问题。
提供多态行为的另一种方法是使用标准库类型,如std::variant,并定义shape,如:
class circle {
int radius;
public:
circle(int radius2) :radius(radius2){ }
void displayArea() {
double area = 3.14*radius*radius;
std::cout << " \n Area circle" << area<<std::endl;
}
};
class triangle {
double a,b,c;
public:
triangle(double a1, double b1, double c1): a(a1), b(b1),c(c1) {
if (a + b > c && a + c > b && b + c > a)
std::cout << "The sides form a triangle" << std::endl;
else
std::cout << "The sides do not form a triangle. Correct me !" << std::endl;
}
void displayArea() {
double s = (a + b + c) / 2;
double area = sqrt(s*(s - a)*(s - b)*(s - c));
std::cout << " \n Area triangle"<< area<<std::endl;
}
};
using shape = std::variant<triangle,circle>;
// Example of how to modify a shape
auto myShape = shape{triangle{1.0, 2.0, 2.0}};
myShape = triangle{3.0, 3.0, 3.0};
并且可以编写一个shape访问函数来调用相应的displayArea。
虽然这样的解决方案更常规,但使用 std::variant 不会在分配给其他类型的形状(除了它定义的形状)时打开,并且像 myShape = rectangle{1.5, 2.0}; 这样的代码不会工作。
我们可以使用std::any,而不是std::variant。这将避免只支持像std::variant 一样定义的形状的缺点。使用此 shape 的代码可能如下所示:
auto myShape = shape{triangle{1.0, 2.0, 2.0}};
myShape = triangle{3.0, 3.0, 3.0};
std::any_cast<triangle&>(mShape).displayArea();
myShape = rectangle{1.5, 2.0};
std::any_cast< rectangle&>(mShape).displayArea();
然而,使用 std::any 的一个缺点是它不会根据这些值的类型必须提供的任何概念功能来限制它可以采用的值。
我将描述的最后一个替代方案是 Sean Parent 在他的演讲 Inheritance Is The Base Class of Evil 和其他地方描述的解决方案。人们似乎决定调用这些类型:多态值类型。我喜欢将此解决方案描述为扩展了更熟悉的 pointer to implementation (PIMPL) 成语。
以下是shape 类型的多态值类型示例(省略了一些内容以便于说明):
class shape;
void displayArea(const shape& value);
class shape {
public:
shape() noexcept = default;
template <typename T>
shape(T arg): m_self{std::make_shared<Model<T>>(std::move(arg))} {}
template <typename T, typename Tp = std::decay_t<T>,
typename = std::enable_if_t<
!std::is_same<Tp, shape>::value && std::is_copy_constructible<Tp>::value
>
>
shape& operator= (T&& other) {
shape(std::forward<T>(other)).swap(*this);
return *this;
}
void swap(shape& other) noexcept {
std::swap(m_self, other.m_self);
}
friend void displayArea(const shape& value) {
if (value.m_self) value.m_self->displayArea_();
}
private:
struct Concept {
virtual ~Concept() = default;
virtual void displayArea_() const = 0;
// add pure virtual functions for any more functionality required for eligible shapes
};
// Model enforces functionality requirements for eligible types.
template <typename T>
struct Model final: Concept {
Model(T arg): data{std::move(arg)} {}
void displayArea_() const override {
displayArea(data);
}
// add overrides of any other virtual functions added to Concept
T data;
};
std::shared_ptr<const Concept> m_self; // Like a PIMPL
};
struct circle {
int radius = 0;
};
// Function & signature required for circle to be eligible instance for shape
void displayArea(const circle& value) {
// code for displaying the circle
}
struct triangle {
double a,b,c;
};
// Function & signature required for triangle to be eligible instance for shape
void displayArea(const triangle& value) {
// code for displaying the triangle
}
// Now we get usage like previously recommended...
auto myShape = shape{triangle{1.0, 2.0, 2.0}}; // triangle data is saved
displayArea(myShape); // calls displayArea(const triangle&)
myShape = circle{4}; // now circle data is stored in myShape
displayArea(myShape); // now calls displayArea(const circle&)
// And changing the settings like a modifyShape function occurs now more regularly
// by using the assignment operator instead of another function name...
mShape = circle{5}; // modifies shape from a circle of radius 4 to radius 5
a link 基本上是这段代码,它显示了代码正在编译,并且这个 shape 也是具有多态行为的非多态类型。
虽然这种技术在使事情正常运转的机制方面带来了负担,但也有人努力让这变得更容易(例如P0201R2)。此外,对于已经熟悉 PIMPL 习语的程序员来说,我不会说这很难接受,就像从引用语义和继承方面的思考转变为值语义和组合方面的思考一样。