== Multiple Inheritance == * Let's review our program that prints graphs in the DOT format. Do you remember how does a DOT file look like? digraph Ex1 { 2 [shape=box, color=blue]; 1 [color=red]; 0 -> 1; 0 -> 2 [dir=none]; } * How can we represent vertices? - Vertices have attributes - The list of attributes for each vertex is not necessarily the same - We might have to add new attributes to the design later, after the program has been already implemented. * What if we had a class to represent each attribute? * How would be the representation of the 'color' attribute? #ifndef COLORABLE_H #define COLORABLE_H class Colorable { public: Colorable(std::string color): _color(color) {} virtual std::string to_string() const { return "color=" + _color; } void setColor(std::string color) { _color = color; } std::string getColor() const { return _color; } private: std::string _color; }; #endif * How could we produce a vertex with a color, i.e., a "Colorable" vertex? #ifndef VERTEX_H #define VERTEX_H class Vertex: public Colorable { public: Vertex(unsigned newId, std::string color): Colorable(color), _id(newId) {} std::string to_string() const override { return std::to_string(getID()) + " [" + Colorable::to_string() + "]"; } unsigned getID() const { return _id; } private: const unsigned _id; }; #endif * Can you write a program to test this design? #include #include "Colorable.h" #include "Vertex.h" int main() { Vertex v0(0, "blue"); std::cout << v0.to_string() << std::endl; } * Our class Vertex has something new: the "override" keyword in the to_string method. What does it do? * What would happen if I removed the 'virtual' keyword from Colorable::to_string? - error: only virtual member functions can be marked 'override' * So, again, what's the benefit of the "override" keyword? * Now, I would like to have also "Shapeable" vertices. How can we add that to our design? * Can you implement a Shapeable class, to accommodate the notion of shapes? #ifndef SHAPEABLE_H #define SHAPEABLE_H class Shapeable { public: Shapeable(std::string shape): _shape(shape) {} virtual std::string to_string() const { return "shape=" + _shape; } void setShape(std::string shape) { _shape = shape; } std::string getShape() const { return _shape; } private: std::string _shape; }; #endif * And how can we add Shapes to vertices? #ifndef VERTEX_H #define VERTEX_H class Vertex: public Colorable, public Shapeable { public: Vertex(unsigned newId, std::string color, std::string shape): Colorable(color), Shapeable(shape), _id(newId) {} std::string to_string() const override { return std::to_string(getID()) + " [" + Colorable::to_string() + ", " + Shapeable::to_string() + "]"; } unsigned getID() const { return _id; } private: const unsigned _id; }; #endif * Can you write a program to test this new extension? #include #include "Colorable.h" #include "Shapeable.h" #include "Vertex.h" int main() { Vertex v0(0, "black", "box"); std::cout << v0.to_string() << std::endl; } * We can move the includes of Colorable and Shapeable to inside Vertex.h. Having headers within headers is not a good practice, but in this case it avoids the need to always import everything that defines Vertices: #ifndef VERTEX_H #define VERTEX_H #include "Shapeable.h" // Imports are here now #include "Colorable.h" class Vertex: public Colorable, public Shapeable { public: Vertex(unsigned newId, std::string color, std::string shape): Colorable(color), Shapeable(shape), _id(newId) {} std::string to_string() const override { return std::to_string(getID()) + " [" + Colorable::to_string() + ", " + Shapeable::to_string() + "]"; } unsigned getID() const { return _id; } private: const unsigned _id; }; #endif ---------------- #include #include "Vertex.h" int main() { Vertex v0(0, "black", "box"); std::cout << v0.to_string() << std::endl; } * Can you use the old Graph class (see the class "modeling") to build a cycle with four nodes? #include #include "Vertex.h" #include "Edge.h" #include "Graph.h" int main() { Vertex v0(0, "black", "box"); Vertex v1(1, "blue", "invtriangle"); Vertex v2(2, "red", "polygon,sides=4,distortion=.7"); Vertex v3(3, "yellow", "box"); Edge e0(&v0, &v1); Edge e1(&v1, &v2); Edge e2(&v2, &v3); Edge e3(&v3, &v0); Graph g0("Ex1"); g0.addEdge(&e0); g0.addEdge(&e1); g0.addEdge(&e2); g0.addEdge(&e3); g0.dump(); } $ clang++ -std=c++11 Graph.cpp Edge.cpp Main.cpp $ ./a.out digraph Ex1 { 3 [color=yellow, shape=box]; 2 [color=red, shape=polygon,sides=4,distortion=.7]; 1 [color=blue, shape=invtriangle]; 0 [color=black, shape=box]; 0 -> 1; 1 -> 2; 2 -> 3; 3 -> 0; } * What's the problem with the design that we have just seen? - It is not trivial to have classes with only some, but not all the attributes. * As an example, add a new attribute "style" to the project. Challenge: you cannot touch Vertex, Graph, or any other class that makes up our DOT library. * Implement a class "Stylable" that encodes the "style" argument: #ifndef STYLABLE_H #define STYLABLE_H class Stylable { public: Stylable(std::string style): _style(style) {} virtual std::string to_string() const { return "style=" + _style; } void setStyle(std::string style) { _style = style; } std::string getStyle() const { return _style; } private: std::string _style; }; #endif * Now, use it to produce a new Vertex3Att class, via multiple inheritance: #ifndef VERTEX_3ATT_H #define VERTEX_3ATT_H #include "Vertex.h" #include "Stylable.h" class Vertex3Att: public Vertex, public Stylable { public: Vertex3Att(unsigned newId, std::string color, std::string shape, std::string style): Vertex(newId, color, shape), Stylable(style) {} std::string to_string() const override { return std::to_string(getID()) + " [" + Colorable::to_string() + ", " + Shapeable::to_string() + ", " + Stylable::to_string() + "]"; } }; #endif * Why do you have two classes? Wouldn't it be easier to simply create a new class that extends Vertex and adds in the new "style" attribute? - We would lose the opportunity to use Style in other contexts. * Now, write a program to test this design: #include #include "Vertex3Att.h" #include "Edge.h" #include "Graph.h" int main() { Vertex3Att v0(0, "yellow", "box", "filled"); Vertex3Att v1(1, "blue", "invtriangle", "\"rounded,filled\""); Vertex3Att v2(2, "red", "polygon,sides=4,distortion=.7", "filled"); Vertex3Att v3(3, "black", "box", "dotted"); Edge e0(&v0, &v1); Edge e1(&v1, &v2); Edge e2(&v2, &v3); Edge e3(&v3, &v0); Graph g0("Ex1"); g0.addEdge(&e0); g0.addEdge(&e1); g0.addEdge(&e2); g0.addEdge(&e3); g0.dump(); } == Composition == * Our design has a few problems. Can you enumerate them? - Low flexibility - Hard to extend * How could we improve it? * Can you create a class Attribute, which encodes a node attribute? class Attribute { public: Attribute(std::string name, std::string value): _name(name), _value(value) { } std::string to_string() const { return _name + "=" + _value; } private: const std::string _name; const std::string _value; }; * Can you now add a vector of attributes to each vertex, plus the code to add attributes to a vertex? class Vertex { public: Vertex(unsigned id): _id(id) {} std::string to_string() const { std::string str = std::to_string(_id) + " ["; for (int i = 0; i < atts.size(); i++) { str += atts[i]->to_string(); if (i < atts.size() - 1) { str += ", "; } } str += "]"; return str; } ~Vertex() { for (Attribute* att: atts) { delete att; } } const unsigned _id; void addAttribute(std::string name, std::string value) { atts.push_back(new Attribute(name, value)); } private: std::vector atts; }; * Can you write a method to create vertices with three attributes: shape, color and style? Vertex* getVertex(std::string color, std::string shape, std::string style) { static int counter = 0; Vertex* v = new Vertex(counter++); v->addAttribute("color", color); v->addAttribute("shape", shape); v->addAttribute("style", style); return v; } * Can you write a driver to test your new design? int main() { Vertex *v0 = getVertex("yellow", "box", "filled"); Vertex *v1 = getVertex("blue", "invtriangle", "\"rounded,filled\""); Vertex *v2 = getVertex("red", "polygon,sides=4,distortion=.7", "filled"); Vertex *v3 = getVertex("black", "box", "dotted"); Edge e0(v0, v1); Edge e1(v1, v2); Edge e2(v2, v3); Edge e3(v3, v0); Graph g0("Ex1"); g0.addEdge(&e0); g0.addEdge(&e1); g0.addEdge(&e2); g0.addEdge(&e3); g0.dump(); delete v0; delete v1; delete v2; delete v3; } * Would it be possible to have the same design, but without having to create a new class? - We could add a map to Vertex! * Can you re-implement Vertex, this time with a map? #ifndef VERTEX_H #define VERTEX_H #include class Vertex { public: Vertex(unsigned id): _id(id) {} std::string to_string() const { std::string str = std::to_string(_id) + " ["; for (auto const& entry: atts) { str += entry.first + "=" + entry.second + ","; } str += "]"; return str; } const unsigned _id; void addAttribute(const std::string name, const std::string value) { atts[name] = value; } private: std::map atts; }; #endif == The Diamond Problem == * What does the program below print? #include using namespace std; class A { public: A(int x) { cout << "A::A(int) called" << endl; } }; class B: public A { public: B(int x): A(x) { cout << "B::B(int) called" << endl; } }; class C: public A { public: C(int x): A(x) { cout << "C::C(int) called" << endl; } }; class D: public B, public C { public: D(int x): B(x), C(x) { cout << "D::D(int) called" << endl; } }; int main() { D d1(42); } * How many times is the constructor of A called? * How could we fix this program? #include using namespace std; class A { public: A(int x) { cout << "A::A(int) called" << endl; } A() { cout << "A::A() called" << endl; } }; class B: virtual public A { public: B(int x): A(x) { cout << "B::B(int) called" << endl; } }; class C: virtual public A { public: C(int x): A(x) { cout << "C::C(int) called" << endl; } }; class D: public B, public C { public: // This implementation calls the default constructor of A: // D(int x): B(x), C(x) { cout << "D::D(int) called" << endl; } // And here we are calling the parameterized constructor: D(int x): B(x), C(x), A(x) { cout << "D::D(int) called" << endl; } }; int main() { D d1(42); }