Now if the requirement comes that one needs to implement modifyShape function... how should this function look like in the base class?
How this function should look is a matter of opinion but let's get around that instead by:
- recognizing how the function could look, and
- basing alternative looks on some "best practices" recommendations.
The C++ Core Guidelines is often referred to as a "best practices" guide and it suggests preferring concrete regular types. We can use that guidance to address the question and provide a way that this function and design could look.
To start with, understand that there are differences between polymorphic types and polymorphic behavior.
Polymorphic types are types that have or inherit at least one virtual function. This shape class and its virtual displayArea member function is such a polymorphic type. In C++ terms, these are all types T for which std:: is_polymorphic_v<T> returns true.
Polymorphic types come with differences from non-polymorphic types in regard to this question like the following:
- They need to be handled by references or pointers to avoid slicing.
- They're not naturally regular. I.e. they can't be treated like a fundamental C++ type like
int.
So the following code won't work with the design you've provided but guidance is that it did work:
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
Meanwhile, it's the polymorphic behavior of shape that's more important so that a shape can be a circle or a triangle for example. Using polymorphic types is a way to provide polymorphic behavior as you show but it's not the only way and it has problems like you're asking about how to solve.
Another way to provide polymorphic behavior is to use a standard library type like std::variant and define shape like:
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};
And one can write a shape visiting function to call the appropriate displayArea.
While such a solution is more regular, using std::variant isn't open on assignment to other kinds of shapes (besides the ones it's defined for) and code like myShape = rectangle{1.5, 2.0}; won't work.
Instead of std::variant, we could use std::any. This would avoid the downside of only supporting the shapes for which it's defined for like with std::variant. Code for using this shape might then look like:
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();
A downside however of using std::any would be that it doesn't restrict the values it can take based on any conceptual functionality the types of those values would have to provide.
The final alternative I'll describe is the solution described by Sean Parent in his talk Inheritance Is The Base Class of Evil and other places. People seem to be settling on calling these kinds of types: polymorphic value types. I like describing this solution as one that extends the more familiar pointer to implementation (PIMPL) idiom.
Here's an example of a polymorphic value type (with some stuff elided for easier exposition) for the shape type:
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
Here's a link to basically this code, that shows the code compiling and that this shape is also a non-polymorphic type with polymorphic behavior.
While this technique carries burden in terms of mechanics to make things work, there are efforts to make this easier (like P0201R2). Additionally, for programmers already familiar with the PIMPL idiom, I wouldn't say this is as difficult to accept, as is the shift from thinking in terms of reference semantics and inheritance, to thinking in terms value semantics and composition.