我正在研究仅标头库的代码库。它包含这个Polygon
类,它具有很大的问题:大约8000行。我正试图将其分解,但是遇到了麻烦。该类和库的几个约束:
我不能随意更改库以要求预编译的部分。这不适合我们当前的构建街道,人们强烈认为它仅是标题。
该类对性能至关重要,它的分配和算法占我正在处理的应用程序总运行时的99%以上。
有时,此类经常被构造(很多三角形),并且经常调用其方法。因此,如果可能的话,我希望它没有虚拟表,也没有追逐指针以进行合成,除非保证编译器(GCC -O2)对此进行了优化。
此类包含对公共功能(例如area()
和contains(Point2)
)中的多边形的若干操作。这些中的每一个都有针对各种用例的几种实现,主要是小多边形与大型多边形,其中小多边形采用幼稚的单线程方法,而大型多边形则运行多线程或使用时间复杂度更高的算法。基本上像这样(简化):
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) { ... }
...
}
我的尝试是将所有这些操作放到一个单独的文件中。这似乎是逻辑上的关注分离,并使代码更具可读性。
到目前为止,我最好的尝试是这样的:
//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) { ... }
但是,这样做的主要缺点是这些子文件不是完整的头文件。它们包含类的一部分,并且永远不应包含在
Polygon
类之外。这不是灾难性的,但是肯定在一年后很难理解。我研究过的替代方法:
Mixins。但是,mixin则无法访问基类中的数据。
自由浮动功能类似于Boost的功能。但是,这有几个问题:自由浮动功能无法访问受保护的字段。这些文件需要相互包含,导致当自由浮动功能需要
Polygon
类时,它们是不完整的类型。需要提供指向多边形的指针(不确定是否会优化吗?)。提供实现类的模板参数。这最终类似于自由浮动函数,因为实现类需要访问
Polygon
的受保护字段,Polygon
在实现需要时是不完整的,并且仍然需要以某种方式提供Polygon
实施。我曾想过要通过继承来实现这一点,其中受保护的数据成员位于私有基类中。子类是详细的实现。然后将有一个公共类,其所有公共功能仍可以调用detail实现。但是,这是刻板的钻石问题,因此需要一个虚拟表。但是没有测试,因为这很难设置。
您认为最好的解决方案是什么?您知道我可以尝试的其他选择吗?
最佳答案
我相信您可以使用“好奇地重复出现的模板模式”(也称为“静态多态性”)。这是一篇很好的文章,介绍了为什么它不是未定义的行为Why is the downcast in CRTP defined behaviour
-
我用一行代码稍微简化了您的示例。这是基类,在这种情况下,它是长度计算功能的实现:
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;
}
};
-
这是主类,它继承自基类并引入Length函数。请注意,要使LineLength访问受保护的成员,它需要成为朋友。如果需要外部函数来访问LineLength,则必须公开继承LineLength。
class Line : public LineLength<Line>
{
protected:
friend class LineLength<Line>;
float start, stop;
public:
Line(float start, float stop): start{start}, stop{stop} {}
};
然后使用以下命令运行它:
int main()
{
Line line{1,3};
return line.Length();
}
该示例可以在此处在线运行:https://onlinegdb.com/BJssU3TUr
以及带有单独标题的实现的版本:https://onlinegdb.com/ry07PnTLB
-
如果您需要访问基类函数,则可以执行类似的操作。
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";
}
};
注意,在类中,您需要通过基本类型(即LineLength :: Length())来评估基本成员函数。
编辑
如果需要使用非常量成员函数,则必须提供Base()函数的非常量重载。
基类的一个示例可以是Collapser。
此功能将停止变量设置为开始变量。
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;
}
};
要使用此代码,请按照应用LineLength的相同方式将其应用于类。
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} {}
};
关于c++ - 在仅 header 库中拆分大文件,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/57966055/