1

I'm working on a code base of a header-only library. It contains this Polygon class which has the problem that it is rather large: About 8000 lines. I'm trying to break this up but have been running into trouble. A couple of constraints for this class and library:

  • I'm not at liberty to change the library to require pre-compiled parts. This doesn't fit in our current build street and people feel rather strongly about it being header-only.
  • The class is very performance critical, its allocations and algorithms comprising over 99% of the total runtime of the application I'm working on.
  • Sometimes this class gets constructed often (many many triangles) and it'll call its methods often. So I'd prefer it to have no virtual table, if possible, and no chasing pointers around for composition unless the compiler (GCC -O2) is guaranteed to optimise this away.

This class contains several operations on polygons in public functions, like area() and contains(Point2). Each of these has several implementations for various use cases, mainly small polygons vs. large ones where the small polygon gets a naive single-threaded approach but the large one runs multithreaded or using an algorithm with better time complexity. Basically something like this (simplified):

class Polygon {
public:
    area_t area() {
        if(size() < 150) return area_single_thread();
        return area_openmp();
    }

    bool contains(Point2 point) {
        if(size() < 75) return contains_single_thread(point);
        if(size() < 6000) return contains_openmp(point);
        return contains_opencl(point);
    }
    ...

private:
    area_t area_single_thread() { ... }
    area_t area_openmp() { ... }
    bool contains_single_thread(Point2 point) { ... }
    bool contains_openmp(Point2 point) { ... }
    bool contains_opencl(Point2 point) { ... }
    ...
}

My attempt is to bring each of these operations into a separate file. This seems like a logical separation of concern and makes the code much more readable.

So far my best attempt is something like this:

//polygon.hpp
class Polygon {
public:
    area_t area() {
        if(size() < 150) return area_single_thread();
        return area_openmp();
    }

    bool contains(Point2 point) {
        if(size() < 75) return contains_single_thread(point);
        if(size() < 6000) return contains_openmp(point);
        return contains_opencl(point);
    }
    ...

private:
//Private implementations separated out to different files for readability.
#include "detail/polygon_area.hpp"
#include "detail/polygon_contains.hpp"
...
}
//polygon_area.hpp
area_t area_single_thread() { ... }
area_t area_openmp() { ... }
//polygon_contains.hpp
bool contains_single_thread(Point2 point) { ... }
bool contains_openmp(Point2 point) { ... }
bool contains_opencl(Point2 point) { ... }

However this has the major disadvantage that these sub-files are not full-fledged header files. They contain part of a class and should never be included outside of the Polygon class. It's not disastrous but is certainly hard to understand some year later.

Alternatives I looked into:

  • Mixins. However the mixin then has no access to the data in the base class.
  • Free-floating functions similar to how Boost does this. However this has several problems: The free-floating function has no access to protected fields. The files need to include each other leading to the Polygon class being incomplete type when the free-floating function needs it. A pointer to the polygon needs to be provided (not sure if this'll get optimised away?).
  • Template arguments providing implementation classes. This ends up similar to the free-floating function in that the implementation class needs access to the protected fields of the Polygon, the Polygon is incomplete when the implementation needs it, and the Polygon still needs to be provided somehow to the implementation.
  • I had a thought to implement this with inheritance, where the protected data members are in a private base class. Subclasses are the detail implementation then. And then there would be one public class with all of the public functions that can still call the detail implementation. However this is the stereotypical diamond problem and would necessitate a virtual table. Didn't test this though, as that is rather hard to set up.

What do you think is the best solution? Do you know any alternatives I could try?

Ghostkeeper
  • 2,830
  • 1
  • 15
  • 27
  • Today, with clangd used as a language server by IDEs for code modeling, splitting the implementations into .inl files and including them at the bottom of header files result in error messages because the header files are not self-contained. See github.com/clangd/clangd/issues/45 . I went down the "split into .inl files" road myself for my own open-source library, and am now faced with this clangd problem. I now have to decide if I want to move the implementations back into the class declarations, or at the bottom of the class declaration within the same .hpp file. – Emile Cormier Apr 20 '22 at 21:17

2 Answers2

2

I believe you can use the Curiously Recurring Template Pattern( Also called static polymorphism ). This is good post about why it isn't undefined behaviour Why is the downcast in CRTP defined behaviour

--

I've somewhat simplified your example with a line. This is the base class, in this case it is the implementation of a length calculation function:

template <typename T>
class LineLength
{
    // This is for non-const member functions
    T & Base(){ return *static_cast<T *>(this); }
    // This is for const member functions
    T const & Base() const { return *static_cast<T const *>(this); }
public:
    float Length() const
    {
        return Base().stop - Base().start;
    }
};

--

This is the main class, it inherits from the base class and brings in the Length function. Note that for LineLength to access the protected members it needs to be a friend.The inheritance of LineLength needs to be public if you need external functions to access it.

class Line : public LineLength<Line>
{
protected:
    friend class LineLength<Line>;
    float start, stop;
public:
    Line(float start, float stop): start{start}, stop{stop} {}
};

Then just run it with this:

int main()
{
    Line line{1,3};
    return line.Length();
}

This example can be run online here: https://onlinegdb.com/BJssU3TUr And a version with the implementation in a seperate header: https://onlinegdb.com/ry07PnTLB

--

If you need to access the base class functions then you can do something similar to this.

class Line : public LineLength<Line>
{
protected:
    friend class LineLength<Line>;
    float start, stop;
public:
    Line(float start, float stop): start{start}, stop{stop} {}

    void PrintLength() const
    {
        std::cout << LineLength<Line>::Length() << "\n";
    }
};

Note that from within the class you'll need to assess the base member function via the base type (ie LineLength::Length() ).

EDIT

If you require the use of non-const member functions then you must provide a non-const overload of the Base() function.

An example of a base class could be Collapser. This function sets the stop variable to the start variable.

template <typename T>
class Collapser
{
    // This is for non-const member functions
    T & Base(){ return *static_cast<T *>(this); }
public:
    void Collapse()
    {
        Base().stop = Base().start;
    }
};

To use this code, apply it to the class in the same way LineLength was applied.

class Line : public LineLength<Line>, public Collapser<Line>
{
protected:
    friend class Collapser<Line>;
    friend class LineLength<Line>;
    float start, stop;
public:
    Line(float start, float stop): start{start}, stop{stop} {}
};
David Ledger
  • 2,033
  • 1
  • 12
  • 27
  • So essentially `LineLength` is a class that gets supplied with a Base class that should match the footprint of the `Line` class. If given the proper Base class, it'll calculate the correct length. I like this solution! It even gives an opportunity for better separation when unit testing by supplying a mock as base class. I'm giving this question another day to see if something simpler to understand for outsiders props up, but so far this looks good. – Ghostkeeper Sep 17 '19 at 22:35
  • 1
    For those who look to this pattern to implement it: If your implementation-class needs to modify the main class' members, the consts need to be removed from the Base() function. – Ghostkeeper Sep 17 '19 at 22:39
  • Indeed, you would need to remove const. You also might want to provide a const and non-const overload. I'll add it to the answer. I've added it to the answer. – David Ledger Sep 18 '19 at 01:45
1

I don't think you do your readers any favors by doing this:

private:
//Private implementations separated out to different files for readability.
#include "detail/polygon_area.hpp"
#include "detail/polygon_contains.hpp"
...

Now your reader has to open up another file to see what's going on under the hood, and still does not have a brief synopsis of the private details of Polygon.

The first thing I recommend is a simple refactoring which simply defines all of your existing member functions out-of-declaration, instead of in-declaration:

class Polygon
{
public:
    area_t area();
    bool contains(Point2 point);
    // ...

private:
    area_t area_single_thread();
    area_t area_openmp();
    bool contains_single_thread(Point2 point);
    bool contains_openmp(Point2 point);
    bool contains_opencl(Point2 point);
    // ...
};

// Implementation

inline
area_t
Polygon::area()
{
    if(size() < 150)
        return area_single_thread();
    return area_openmp();
}

inline
bool
Polygon::contains(Point2 point)
{
    if(size() < 75)
        return contains_single_thread(point);
    if(size() < 6000)
        return contains_openmp(point);
    return contains_opencl(point);
}

inline
area_t
Polygon::area_single_thread() { /*...*/ }

inline
area_t
Polygon::area_openmp() { /*...*/ }

inline
bool
Polygon::contains_single_thread(Point2 point) { /*...*/ }

inline
bool
Polygon::contains_openmp(Point2 point) { /*...*/ }

inline
bool
Polygon::contains_opencl(Point2 point) { /*...*/ }

This has zero impact on functionality or efficiency, but aids readability in separating interface from implementation. It also does not punish compile time by opening spurious "implementation headers". Opening files is one of the more expensive things a compiler can do.

Now that this is accomplished, on to a more subtle point: inline is only a hint, and your compiler is free to take the hint or not. Here inline serves mainly to mark your functions as having "weak linkage" so that your linker doesn't complain about duplicate definitions when this header gets included into more than one source.

So you can stay with this design, which may "hint" to mark some functions as inline which are really to big to inline, and trust that your compiler won't. Or you can choose another technique for giving your code "weak linkage" without suggesting to your compiler that the functions be inlined:

template <class WeakLinkage = void>
class Polygon
{
public:
    area_t area();
    bool contains(Point2 point);
    // ...
    int size() const;

private:
    area_t area_single_thread();
    area_t area_openmp();
    bool contains_single_thread(Point2 point);
    bool contains_openmp(Point2 point);
    bool contains_opencl(Point2 point);
    // ...
};

// Implementation

template <class WeakLinkage>
area_t
Polygon<WeakLinkage>::area()
{
    if(size() < 150)
        return area_single_thread();
    return area_openmp();
}

// ...

I've used a gratuitous, defaulted template parameter to give the type's member functions weak linkage without declaring the member functions inline. In C++17 this just worksTM.

Prior to C++17, you'll need to rename Polygon to something like PolygonImp and then provide this using declaration:

using Polygon = PolygonImp<>;

And with the gratuitous template technique, you can still mark some of your member functions with inline if you would like to hint to the compiler that they should be inlined.

Which is best is going to depend on your compiler. But this strategy is a straight-forward and mechanical extension of your current design that doesn't add expense, and does separate your interface from your implementation, which can have readability advantages. Real world libraries are known to use this technique.1, 2

Sometimes a compromise is made when a member function can be declared and defined on a single line:

class Polygon
{
public:
    // ...
    int size() const {return size_;}
    // ...
};
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 1
    Neat, Its great when people put different answers. Good points on inline which I forgot to add to the why the CRTP works regarding the linker. Why doesn't your int size() const { return size_;} function shown at the very end need an inline hint? – David Ledger Sep 17 '19 at 06:59
  • Member functions defined within the class declaration are implicitly inline. ... Which is another reason for not defining everything within the class declaration. – Howard Hinnant Sep 17 '19 at 13:14
  • These options are definitely improvements and I'll apply them in part. However they don't allow putting part of the implementation in different files. At least, except by taking the #include approach to simply chop the file in pieces as I was doing. Compilation time is not a real constraint to my team. Using 8000 line header files in general probably takes a lot of time already. While this is better, it's not a complete solution yet to me. – Ghostkeeper Sep 17 '19 at 21:58