Practical Pearls

Abstracting Generic Operations in C++ Classes

December 27, 2017

The Traditional Style

Class design in C++ follows an approach in which we add member variables, add accessors to them, and define functions to operate on the object of that class. This conventional approach does not utilize the power of C++ templates and modern compilers which are capable of automatically generating such code with a little help from us. Let me explain this with an example. In this post we will use the terms class and struct interchangeably.

Suppose we were to design a drawing application in C++. The normal scheme of design would proceed with defining a root class, in this case – Shape, along with classes like Line, Rectangle, and Polygon. Each shape will have a id attribute, a name attribute and a lineWeight attribute defined on it. Listing 1 shows the implementation of the shape hierarchy of our drawing application.

// Listing 1. shapes.h
#include <string>
#include <list>
#include <utility>

// root of the shape hierarchy
struct Shape
{
    Shape() {}
    Shape(int id, std::string name, double lineWeight) 
    : _id(id), _name(name), _lineWeight(lineWeight)
    {}
    
    int getId()
    {
        return _id;
    }

    double getLineWeight()
    {
        return _lineWeight;
    }

    std::string getName()
    {
        return _name;
    }

    void setId(int id)
    {
        _id = id;
    }

    void setLineWeight(double lineWeight)
    {
        _lineWeight = lineWeight;
    }

    void setName(std::string &amp;name)
    {
        _name = name;
    }

private:
    int _id;
    std::string _name;
    double _lineWeight;
};

struct Line : public Shape
{
public:
    Line(){}

    Line(int id, std::string name, double lineWeight) 
    : Shape(id, name, lineWeight)
    {}    
};

typedef std::list<std::pair<int, int> > PointsT;
struct Polygon : public Shape
{
public:
    Polygon(){}

    Polygon(int id, 
            std::string name, 
            double lineWeight, 
            PointsT &amp;points)
    : Shape(id, name, lineWeight)
    {
        _ppoints = new PointsT(points.size());
        std::copy(points.begin(), points.end(), _ppoints->begin());
    }
private:
    PointsT *_ppoints;
};

Now that we have defined our classes let’s add some functionality. As a feature, our application would allow users to search through the shapes in a drawing, based on values of the attributes defined on them, and will return the first shape that matches a given criterion. For example, one might want to search for a shape with the name “circle1”. To implement this our application will maintain a list of shapes that are present in a drawing and will define search functions on the list of shapes. We will adopt a function-object based approach for defining the search functionality. Listing 2 illustrates these search functors.

// Listing 2. operations.h
#include "shapes.h"

// functor that matches against a given name
struct nameMatcher
{
    std::string _name;

    nameMatcher(std::string name) 
    : _name(name) {}

    bool operator()(Shape &amp;shape)
    {
        return shape.getName() == _name;
    }

    bool operator()(Shape *shape)
    {
        return shape->getName() == _name;
    }
};

// functor that matches against a given ID
struct idMatcher
{
    int _id;

    idMatcher(int id) 
    : _id(id) {}

    bool operator()(Shape &amp;shape)
    {
        return shape.getId() == _id;
    }

    bool operator()(Shape *shape)
    {
        return shape->getId() == _id;
    }
};

// functor to match a shape that has a specific line weight
struct lineWeightMatcher
{
    double _lineWeight;

    lineWeightMatcher(double lineWeight) 
    : _lineWeight(lineWeight) {}

    bool operator()(Shape &amp;shape)
    {
        return shape.getLineWeight() == _lineWeight;
    }

    bool operator()(Shape *shape)
    {
        return shape->getLineWeight() == _lineWeight;
    }
};

To put our drawing application to action we define the main function. Though we call it a drawing application, we will not focus on the drawing code, instead for the sake of brevity we will add some shapes and perform searching through these shapes as shown in listing 3.

// Listing 3. main.cpp

#include "shapes.h"
#include "operations.h"

#include <cassert>
#include <list>
#include <iostream>
#include <algorithm>

int main()
{
    Line l0(1, "l1", 0);
    Line l1(2, "this is new", 0);
    Line l2(3, "the third line", 5);

    PointsT pts;
    pts.push_back(PointsT::value_type(1, 1));
    pts.push_back(PointsT::value_type(2, 2));
    pts.push_back(PointsT::value_type(3, 4));
    pts.push_back(PointsT::value_type(5, 6));

    Polygon p(4, "Polygon", 2, pts);

    std::list<Shape> shapes;

    shapes.push_back(l0);
    shapes.push_back(l1);
    shapes.push_back(l2);
    shapes.push_back(p);

    // create a id based matcher
    idMatcher m(2);
    std::list<Shape>::iterator it = std::find_if(shapes.begin(), 
                                                 shapes.end(), 
                                                 m);
    assert(it != shapes.end());
    std::cout << (*it).getId() << std::endl;

    // create a name based matcher
    nameMatcher m1("Polygon");
    it = std::find_if(shapes.begin(), shapes.end(), m1);
    assert(it != shapes.end());
    std::cout << (*it).getName() << std::endl;

    // create a line weight based matcher
    lineWeightMatcher m2(5.0);
    it = std::find_if(shapes.begin(), shapes.end(), m2);
    assert(it != shapes.end());
    std::cout << (*it).getLineWeight() << std::endl;

    return 0;
}

The New Way

A close observation of the previous technique reveals a pattern in the accessor and functor code. The principle design guideline for generic programming is that of identifying patterns in our code that occur in more than one place. In this context, the pattern that emerges most prominently is that of the getMember and setMember. Having identified these pattern, we can now write generic code that abstract away these patterns as templates and in the process end up harnessing the compiler to create code for us that is type safe.

We will be employing boost variants, and metaprogramming tricks. Our approach here will be of defining new types as opposed to adding member variables in the classes. Additionally, an enum AttribIndex will be used to define the position of a particular attribute value in a hierarchy-wide attribTable.

// Listing 4. attributes.h

#include <list>
#include <utility>

typedef enum 
{
    UNKNOWN = -1,
    NAME = 0,
    WEIGHT,
    ID,
    POINTS,
    MAX_ATTRIBS
} AttribIndex;

// name assigned to an object
struct AttrName
{
    enum {Idx = NAME};
    typedef std::string Type;
};

// id of the object
struct AttrId
{
    enum {Idx = ID};
    typedef int Type;
};

// line weight
struct AttrWeight
{
    enum {Idx = WEIGHT};
    typedef double Type;
};

typedef std::list<std::pair<int, int> > PointsT;
// point list
struct AttrPoints
{
    enum {Idx = POINTS};
    typedef PointsT* Type;
};

The shape hierarchy will now look a bit different. At the outset we will define a variant type VT over the set of types that our class attributes might assume. For each attribute that we would like to add to a class, we will call the appropriate set function in the class’ constructor (listing 5. lines 39, 40, 41). Notice the generic accessors that we define in the Root class in listing 5. The net result – the classes in our shape hierarchy do not contain any member variables! We will soon see how this works to our advantage.

// Listing 5. shapesAdvanced.h

#include <boost/variant/variant.hpp>
#include <boost/variant/get.hpp>

#include <string>
#include <vector>

#include <cassert>
#include <iostream>
#include <algorithm>

#include "attributes.h"

typedef boost::variant< int, double, std::string, PointsT* > VT;

struct Root
{
    VT attribTable[MAX_ATTRIBS];

    template<typename T>
    typename T::Type get()
    {
        return boost::get<T::Type>(attribTable[T::Idx]);
    }

    template<typename T>
    void set(typename T::Type v)
    {
        attribTable[T::Idx] = v;
    }
};

struct Shape : public Root
{
    Shape(){}

    Shape(int id, std::string name, double strokeWidth)
    {
        set<AttrId>(id);
        set<AttrName>(name);
        set<AttrWeight>(strokeWidth);
    }
};

struct Line : public Shape
{
public:
    Line(){}

    Line(int id, std::string name, double lineWeight) 
    : Shape(id, name, lineWeight)
    {}    
};

struct Polygon : public Shape
{
public:
    Polygon(){}

    Polygon(int id, 
            std::string name, 
            double lineWeight, 
            PointsT &amp;points)
    : Shape(id, name, lineWeight)
    {
        PointsT *ppoints = new PointsT(points.size());
        std::copy(points.begin(), 
                  points.end(), 
                  ppoints->begin());
        set<AttrPoints>(ppoints);
    }
};

Let us now define the search functionality in the new system. In the previous version, we had defined multiple functors, one each for the attribute that we had to search for. Why? Due to the very fact that each member had its own getter and setter, and a call to these functions had to be explicitly coded in the functors. In the new version we have replaced member variables with types conforming to a predefined contract. According to this contract each type will have a enum value Idx that specifies its position in the attribute table, attribTable, and expose a type definition Type depicting the type of the value that the attribute can hold. This design lays the ground-stone for defining generic accessors and search mechanism wherein we pass the attribute type as a template argument to these functions as shown in listing 6.

// Listing 6. matcher.h

template<typename AttrType>
struct matcher
{
    typename AttrType::Type d;

    matcher(typename AttrType::Type t) : d(t) {}

    template<typename Obj>
    bool operator()(Obj &amp;obj)
    {
        return obj.get<AttrType>() == d;
    }

    template<typename Obj>
    bool operator()(Obj *obj)
    {
        return obj->get<AttrType>() == d;
    }
};

With our arsenal in place we now proceed to define the main function. Notice how we wrote a single matcher [listing 6] to suffice all of our search functionality. The important thing to remember is that the for each attribute that we would like to define, the corresponding Type should be equality comparable, or in other words should have the operator== defined.

// Listing 7. main.cpp

#include "shapesAdvanced.h"
#include "attributes.h"
#include "matcher.h"

#include <iostream>
#include <list>
#include <algorithm>

int main()
{
    Line l0(1, "l1", 0);
    Line l1(2, "this is new", 0);
    Line l2(3, "the third line", 5);

    PointsT pts;
    pts.push_back(PointsT::value_type(1, 1));
    pts.push_back(PointsT::value_type(2, 2));
    pts.push_back(PointsT::value_type(3, 4));
    pts.push_back(PointsT::value_type(5, 6));

    Polygon p(4, "Polygon", 2, pts);

    std::list<Shape> shapes;

    shapes.push_back(l0);
    shapes.push_back(l1);
    shapes.push_back(l2);
    shapes.push_back(p);

    // create a id based matcher
    matcher<AttrId> m(2);
    std::list<Shape>::iterator it = std::find_if(shapes.begin(), 
                                                 shapes.end(), 
                                                 m);
    assert(it != shapes.end());
    std::cout << (*it).get<AttrId>() << std::endl;

    // create a name based matcher
    matcher<AttrName> m1("Polygon");
    it = std::find_if(shapes.begin(), shapes.end(), m1);
    assert(it != shapes.end());
    std::cout << (*it).get<AttrName>() << std::endl;

    // create a line weight based matcher
    matcher<AttrWeight> m2(5);
    it = std::find_if(shapes.begin(), shapes.end(), m1);
    assert(it != shapes.end());
    std::cout << (*it).get<AttrWeight>() << std::endl;

    return 0;
}

On the face of it, listing 7 does not seem to offer any significant advantage over the traditional approach. Let’s dig deeper.

On popular demand, our drawing objects now need to support a fill color attribute. After all, what good is a drawing application that does not support coloring shapes. In the traditional approach to achieve this we would go about:

  1. defining a Color type as a enum of color values,
  2. add a new member variable _color in the Shape class,
  3. add a pair of accessor functions to this field,
  4. modify the constructors of classes Shape, Line, and Polygon to accept a color value as an additional argument,
  5. create a colorMatcher functor in the same lines as that of the other matcher functors

Using the new technique, to enable the same functionality, we would do the following:

  1. define a Color type as a enum of color values,
  2. add a COLOR item to the AttribIndex enum,
  3. add a new struct AttrColor with Idx = COLOR and typedef int Type,
  4. modify the constructors as we had done in the traditional approach and add a call to set(color)

That’s it! We do not need to code a separate matcher. All our existing code will understand the newly added AttrColor attribute. The manner in which we implemented color support in our classes might not be a paragon of design technique, but serves the purpose of illustrating the advantages of the design under discussion. This design allows us to write general purpose functions that operate on the type hierarchy rooted at the Shape class.

The downside of this approach is that we will add an extra attribute table in every member of the Shape class (which include any class derived from the Shape class); the size of this attribute table will be proportional to the total number of attributes that is defined in all the types belonging to this hierarchy rooted at the Shape class. In the example shown here we had used boost::get. To mitigate the issues that might arise from incompatible types in boost::get calls, an alternative method would be to employ compile-time type-safe visitation mechanism defined here.

Epilogue


The crux of the approach adopted here lies in creating new types for each attribute of a class instead of adding a new member variable, and defining generic functions which operate on those types.