c++c++17header-only

Splitting up large file in header-only library


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:

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:

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


Solution

  • 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} {}
    };