3

I am trying to understand the is-a vs is-like-a relationship where I read somewhere that we must try to follow design such that we always have is-a relationship and not is-like-a. Consider the classic example of shape base class and derived triangle and circle classes. So circle is-a shape and so is triangle is-a shape. The function display area was defined in the base class. Now the below program runs fine.

#include "stdafx.h"
#include <cmath>
#include <iostream>

class shape
{
public:
    virtual void displayArea()=0;
};

class circle :public shape
{
    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 :public shape
{
    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;
    }
};

void main()
{
    shape * p1[2];
    p1[0]= new circle(20);

    p1[1] = new triangle(5.6,8.1,10.3);
    for (int i = 0; i < 2; ++i)
    {
        p1[i]->displayArea();
    }

    int y;
    std::cin >> y;
}

Now if the requirement comes that one needs to implement modifyShape function where each parameter of the shape is modified based on parameter of the user then how should I change my classes such that my is-a relationship is not altered. When I look at it, I feel I will have to define a single argument modifyShape in circle and a 3-argument modifyShape in triangle. But how should this function look like in the base class?

Option 1: I define both single argument and two argument modifyShape function in shape but that would mean i would be having an extra 2 argument function in circle and an extra 1 argument function in triangle.

Option 2: I define a variable argument function modifyShape in shape but somehow this is not looking cleaner to me.

Louis Langholtz
  • 2,913
  • 3
  • 17
  • 40
MAG
  • 2,841
  • 6
  • 27
  • 47
  • 1
    I have never heard about is-like-a. How would you define this relationship? – n. m. could be an AI Jun 21 '17 at 15:37
  • "modifyShape function where each parameter of the shape is modified based on parameter of the user" This doesn't look like a well-stated requirement. What does "based on parameter of the user" mean exactly? If the user enters 5, and we have a unit triangle centeted at (7, 8) and a unit circle centered at (1, -2), what is the expected result? – n. m. could be an AI Jun 21 '17 at 15:43
  • 1
    Think about this from an API perspective. What would modifyShape do? Would you pass in all new parameters? If so, why not create move and copy constructors instead? – Alex Shirley Nov 04 '20 at 18:48
  • You could make modify shape take a variable number of arguments ( https://en.cppreference.com/w/cpp/utility/variadic ). You could use an initializer list ( https://en.cppreference.com/w/cpp/utility/initializer_list ). But I don't think I would. I can't think of a situation in which I would be calling modifyShape on a pointer to Shape without knowing it's a Triangle or Circle -- in fact, you *can't*, because if you don't know what kind of Shape it is you don't know how many parameters to pass. – Topological Sort Mar 31 '23 at 15:25
  • 1
    Maybe if you were loading from a file with lines like `Triangle 2 3 5`, but if you did, it would make sense to say `if (typestring=="Triangle") ((Triangle*) shapeptr)->modifyTriangle(arg1, arg2, arg3);`. – Topological Sort Mar 31 '23 at 15:27

3 Answers3

0

There is a third option you can use, you can crate a new hierarchy of classes (or structs) that will represent the parameters of each shape. Then you can pass the pointer to the base class as an argument to the virtual function. For example:

struct ShapeParams
{
     ...
}

struct TriangleParams : public ShapeParams
{
     double a;
     double b;
     double c:
}
class shape
{
  public:
    virtual void displayArea()=0;
    modifyShape (ShapeParams*) = 0;
};

class triangle :public shape
{
  public:
     void modifyShape (ShapeParams*) = override;

  private:
     TriangleParams m_params;
}
  • 2
    This is a bad advice. The modifyShape implementation would have to cast the ShapeParams parameter. It is error-prone, and you gain nothing. You'll still need to pass TriangleParams to a triangle. – Lior Kogan Jun 21 '17 at 04:20
  • I will indeed need a cast, but it will be done in scope that works always with the same type of ShapeParams. It can be dangerous if you pass a pointer to some other class, but this can be said about almost anything. What you do gain that way is a virtual call that can be used to generalize the work with shapes. Can be handy for example if you use Template Method Design Pattern. – user8160628 Jun 21 '17 at 05:49
0

You could restructure you classes a little bit but it would require another independent class. You can create a set of 2D and 3D math vector class but you will need to have all the overloaded operators and math functions that vectors can do such as add, subtract, multiply by vector or by scalar and if by vector you have the dot and the cross product to worry about. You would need normalize methods, length and such. Once you have these working math vector classes. Then you can redesign your shape classes using the vectors instead. Or instead of writing your own vector class you can use a math library class such as GLM's math library that is used for working in OpenGL. It is free and open source and it is also a header only library. Once you install the library to a path all you need to do is include its header. You do not have to worry about linking. Then with these vector classes it would make the math in your shape classes easier to do, and it would be easier to design the shape classes: Here's an example of what pseudo code would look like:

#include <glm\glm.hpp>
// Needed If Doing Matrix Transformations: Rotation, Translation Scaling etc.
// #include <glm\gtc\matrix_transform.hpp> 

class Shape {
public:
    enum Type {
        NONE = 0,
        TRIANGLE,
        SQUARE,
        CIRCLE,
     };
protected:
    Type type_;
    glm::vec4 color_ { 1.0f, 1.0f, 1.0f, 1.0f }; // Initialize List Set To White By Default
    double perimeter_; // Also Circumference for Circle
    double area_;     
    // double volume_; // If in 3D.
public:
     // Default Constructor
     Shape() : type_( NONE ), color_( glm::vec4( 1.0f, 1.0f, 1.0f, 1.0f ) ) {}       
     // User Defined Constructors
     // Sets Shape Type Only Color Is Optional & By Default Is White
     explicit Shape( Type type, glm::vec4 color = glm::vec4() ) : type_(type), color_( color ) {}

     Type getType() const { return type_; }
     void setType( Shape::Type type ) {
         if ( type_ == NONE ) {
             // Its okay to set a new shape type
             type_ = type;
          } 

          // We Already Have a Defined Shape
          return;
      }

      // Getters That Are Commonly Found Across All Shapes
      double getPerimeter() const { return perimeter_; }
      double getArea() const { return area_; }

      // Common Functions that can be done to any shape
      void setSolidColor( glm::vec4 color ) { color_ = color };
      glm::vec4 getColor() const { return color; }

      // Common Interface That All Shapes Share But Must Override
      virtual double calculateArea() = 0;
      virtual double calculatePerimeter() = 0; 

      // Since we do not know what kind of shape to modify until we have one
      // to work with, we do not know how many parameters this function will need.
      // To get around this we can use a function template and then have overloads 
      // for each type we support
      template<typename Type = Shape>
      virtual void modify( Type* pShape /*,glm::vec3... params*/ );

      // Overloaded Types: - Should Be Defined & Overridden By the Derived Class
      virtual void modify<Triangle>( Triangle* pTriangle, glm::vec3, glm::vec3, glm::vec3, glm::vec4 = glm::vec4() ) { /* ... */ }
      virtual void modify<Circle>( Cirlce* pCircle, float radius, glm::vec4 color = glm::vec4() ) { /* ... * / }

};

Then an Inherited class would look something like:

class Triangle : public Shape {
public:
     // Could Be An Option To Where This is a base class as well to specific types of triangles:
     enum TriangleType {
         Acute = 0,
         Right,
         Equilateral,
         Obtuse
     } // then each of these would have properties specific to each type
private:
    glm::vec3[3] vertices_;

public:
    // Default Constructor
    Triangle() : Shape(TRIANGLE) {} // Sets The Shape Type But Has No Vertices Or Area; just default construction
    // Vertices But No Color
    Triangle( glm::vec3 A, glm::vec3 B, glm::vec3 C ) : Shape(TRIANGLE) {
        vertices_[0] = A;
        vertices_[1] = B;
        vettices_[2] = C;

        // Call These To Have These Values
        calculatePerimeter();
        calculateArea();            
    }
    // Vertices & Color
    Triangle( glm::vec3 A, glm::vec3 B, glm::vec3 C, glm::vec4 color ) : Shape(TRIANGLE) {
        vertices_[0] = A;
        vertices_[1] = B;
        vertices_[2] = C;

        calculatePerimeter();
        calculateArea();
     }

     // No Need To Do The Set & Get Colors - Base Class Does that for you.

     // Methods that this shape must implement
     virtual double calculateArea() override {
         // Calculations For Getting Area of A Triangle
         area_ = /* calculation */;
     };
     virtual double calculatePerimeter() override {
         // Calculations For Getting Perimeter of A Triangle
         perimeter_ = /* calculation */;
     };

     void modify<Triangle>( Triangle* pTriangle, glm::vec3, glm::vec3, glm::vec3, glm::vec4 = glm::vec4() ) override { /* ... */ }

};

Now as for displaying the information; personally I would not implement this in these classes. Just use your standard std::cout or std::ofstream etc. to print the values to the screen or file just buy using the getters such as this:

#include <iostream>
#include "Triangle.h"

int main() {
    Triangle t1( glm::vec3( 0.0f, 1.0f, -1.3f ),   // Vertex A
                 glm::vec3( 3.2f, 5.5f, -8.9f ),   //        B
                 glm::vec3( -4.5f, 7.6f, 8.2f ),   //        C
                 glm::vec4( 0.8f, 0.9f, 0.23f, 1.0f ) ); // Color

    std::cout << "Perimeter is " << t1.getPerimeter() << std::endl;
    std::cout << "Area is " << t1.getArea() << std::endl;

    return 0;
}
Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
0

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:

  1. recognizing how the function could look, and
  2. 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:

  1. They need to be handled by references or pointers to avoid slicing.
  2. 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.

Louis Langholtz
  • 2,913
  • 3
  • 17
  • 40