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
, thePolygon
is incomplete when the implementation needs it, and thePolygon
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?