拆分 header-only 库中的大文件
Splitting up large file in header-only library
我正在研究 header-only 库的代码库。它包含此 Polygon
class,但问题是它相当大:大约 8000 行。我正试图打破它,但 运行 遇到了麻烦。此 class 和库的一些限制条件:
- 我不能随意更改库以需要 pre-compiled 部分。这不适合我们当前的建筑街道,人们强烈认为它是 header-only.
- class 对性能非常关键,它的分配和算法占我正在处理的应用程序总运行时间的 99% 以上。
- 有时这个 class 经常被构造(许多三角形)并且它会经常调用它的方法。所以我更希望它没有虚拟 table,如果可能的话,并且没有为组合而追逐指针,除非编译器 (GCC -O2) 保证优化它。
此 class 包含 public 函数中的多边形操作,例如 area()
和 contains(Point2)
。 其中每一个都有几个实现 用于各种用例,主要是小多边形与大多边形,其中小多边形采用简单的 single-threaded 方法,但大多边形运行多线程或使用具有更好时间复杂度的算法。基本上是这样的(简化):
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) { ... }
然而,这有一个主要缺点,即这些 sub-files 不是 full-fledged header 文件。它们包含 class 的一部分,绝不能包含在 Polygon
class 之外。这不是灾难性的,但几年后肯定很难理解。
我研究的备选方案:
- 混合。然而,mixin 无法访问基 class.
中的数据
- Free-floating 的功能类似于 Boost 执行此操作的方式。然而,这有几个问题: free-floating 函数无法访问受保护的字段。这些文件需要相互包含,导致
Polygon
class 在 free-floating 函数需要时成为不完整的类型。需要提供指向多边形的指针(不确定这是否会得到优化?)。
- 提供实现的模板参数 classes。这最终类似于 free-floating 函数,因为实现 class 需要访问
Polygon
的受保护字段,当实现需要它时 Polygon
是不完整的,并且Polygon
仍然需要以某种方式提供给实现。
- 我想通过继承来实现这一点,其中受保护的数据成员位于私有基础中 class。 Subclasses 就是详细的实现。然后会有一个 public class 和所有 public 函数仍然可以调用细节实现。然而,这是典型的钻石问题,需要虚拟 table。不过没有对此进行测试,因为这很难设置。
您认为最好的解决方案是什么?你知道我可以尝试的任何替代方法吗?
我相信您可以使用 Curiously Recurring Template Pattern(也称为静态多态性)。这很好 post 关于为什么它不是未定义的行为 Why is the downcast in CRTP defined behaviour
--
我用一行简化了您的示例。这是基础class,在本例中它是长度计算函数的实现:
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;
}
};
--
这是主要的class,它继承了基础class并引入了Length函数。请注意,对于 LineLength 访问受保护的成员,它需要是 friend.The LineLength 的继承需要是 public 如果您需要外部函数来访问它。
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
以及在单独 header 中实现的版本:https://onlinegdb.com/ry07PnTLB
--
如果您需要访问基础 class 函数,那么您可以执行类似的操作。
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";
}
};
请注意,在 class 中,您需要通过基本类型(即 LineLength::Length() )评估基本成员函数。
编辑
如果您需要使用 non-const 成员函数,那么您必须提供 Base() 函数的 non-const 重载。
基础 class 的一个示例可以是 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。
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} {}
};
我不认为这样做对 reader 有任何好处:
private:
//Private implementations separated out to different files for readability.
#include "detail/polygon_area.hpp"
#include "detail/polygon_contains.hpp"
...
现在你的 reader 必须打开另一个文件才能看到幕后发生的事情,并且仍然没有 Polygon
.[=24= 的私人细节的简要概要]
我推荐的第一件事是简单的重构,它简单地定义所有现有的成员函数 out-of-declaration,而不是 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) { /*...*/ }
这对功能或效率的影响为零,但有助于将接口与实现分开的可读性。它还 不会 通过打开虚假的 "implementation headers" 来惩罚编译时间。打开文件是编译器可以做的更昂贵的事情之一。
现在这已经完成了,更微妙的一点是:inline
只是一个提示,您的编译器可以自由选择是否接受提示。这里 inline
主要用于将您的函数标记为具有 "weak linkage",这样当此 header 包含在多个源中时,您的链接器不会抱怨重复定义。
所以您可以保留这种设计,它可能 "hint" 将某些函数标记为 inline
,这些函数确实太大而无法内联,并且相信您的编译器不会这样做。或者您可以选择另一种技术来提供代码 "weak linkage" 而无需向编译器建议内联函数:
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();
}
// ...
我使用了一个无偿的默认模板参数来为类型的成员函数提供弱链接,而无需声明成员函数 inline
。在 C++17 中,这个 可以正常工作 TM.
在 C++17 之前,您需要将 Polygon
重命名为 PolygonImp
之类的名称,然后使用声明提供此名称:
using Polygon = PolygonImp<>;
并且使用无偿模板技术,如果您想向编译器提示它们应该被内联,您仍然可以使用 inline
标记您的一些成员函数。
哪个最好取决于您的编译器。但此策略是您当前设计的 straight-forward 和机械扩展,不会增加费用,并且确实将您的界面与您的实现分开,这可能具有可读性优势。众所周知,真实世界的图书馆会使用这种技术。1, 2
有时可以在一行中声明和定义成员函数时做出折衷:
class Polygon
{
public:
// ...
int size() const {return size_;}
// ...
};
我正在研究 header-only 库的代码库。它包含此 Polygon
class,但问题是它相当大:大约 8000 行。我正试图打破它,但 运行 遇到了麻烦。此 class 和库的一些限制条件:
- 我不能随意更改库以需要 pre-compiled 部分。这不适合我们当前的建筑街道,人们强烈认为它是 header-only.
- class 对性能非常关键,它的分配和算法占我正在处理的应用程序总运行时间的 99% 以上。
- 有时这个 class 经常被构造(许多三角形)并且它会经常调用它的方法。所以我更希望它没有虚拟 table,如果可能的话,并且没有为组合而追逐指针,除非编译器 (GCC -O2) 保证优化它。
此 class 包含 public 函数中的多边形操作,例如 area()
和 contains(Point2)
。 其中每一个都有几个实现 用于各种用例,主要是小多边形与大多边形,其中小多边形采用简单的 single-threaded 方法,但大多边形运行多线程或使用具有更好时间复杂度的算法。基本上是这样的(简化):
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) { ... }
然而,这有一个主要缺点,即这些 sub-files 不是 full-fledged header 文件。它们包含 class 的一部分,绝不能包含在 Polygon
class 之外。这不是灾难性的,但几年后肯定很难理解。
我研究的备选方案:
- 混合。然而,mixin 无法访问基 class. 中的数据
- Free-floating 的功能类似于 Boost 执行此操作的方式。然而,这有几个问题: free-floating 函数无法访问受保护的字段。这些文件需要相互包含,导致
Polygon
class 在 free-floating 函数需要时成为不完整的类型。需要提供指向多边形的指针(不确定这是否会得到优化?)。 - 提供实现的模板参数 classes。这最终类似于 free-floating 函数,因为实现 class 需要访问
Polygon
的受保护字段,当实现需要它时Polygon
是不完整的,并且Polygon
仍然需要以某种方式提供给实现。 - 我想通过继承来实现这一点,其中受保护的数据成员位于私有基础中 class。 Subclasses 就是详细的实现。然后会有一个 public class 和所有 public 函数仍然可以调用细节实现。然而,这是典型的钻石问题,需要虚拟 table。不过没有对此进行测试,因为这很难设置。
您认为最好的解决方案是什么?你知道我可以尝试的任何替代方法吗?
我相信您可以使用 Curiously Recurring Template Pattern(也称为静态多态性)。这很好 post 关于为什么它不是未定义的行为 Why is the downcast in CRTP defined behaviour
--
我用一行简化了您的示例。这是基础class,在本例中它是长度计算函数的实现:
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;
}
};
--
这是主要的class,它继承了基础class并引入了Length函数。请注意,对于 LineLength 访问受保护的成员,它需要是 friend.The LineLength 的继承需要是 public 如果您需要外部函数来访问它。
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 以及在单独 header 中实现的版本:https://onlinegdb.com/ry07PnTLB
--
如果您需要访问基础 class 函数,那么您可以执行类似的操作。
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";
}
};
请注意,在 class 中,您需要通过基本类型(即 LineLength::Length() )评估基本成员函数。
编辑
如果您需要使用 non-const 成员函数,那么您必须提供 Base() 函数的 non-const 重载。
基础 class 的一个示例可以是 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。
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} {}
};
我不认为这样做对 reader 有任何好处:
private:
//Private implementations separated out to different files for readability.
#include "detail/polygon_area.hpp"
#include "detail/polygon_contains.hpp"
...
现在你的 reader 必须打开另一个文件才能看到幕后发生的事情,并且仍然没有 Polygon
.[=24= 的私人细节的简要概要]
我推荐的第一件事是简单的重构,它简单地定义所有现有的成员函数 out-of-declaration,而不是 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) { /*...*/ }
这对功能或效率的影响为零,但有助于将接口与实现分开的可读性。它还 不会 通过打开虚假的 "implementation headers" 来惩罚编译时间。打开文件是编译器可以做的更昂贵的事情之一。
现在这已经完成了,更微妙的一点是:inline
只是一个提示,您的编译器可以自由选择是否接受提示。这里 inline
主要用于将您的函数标记为具有 "weak linkage",这样当此 header 包含在多个源中时,您的链接器不会抱怨重复定义。
所以您可以保留这种设计,它可能 "hint" 将某些函数标记为 inline
,这些函数确实太大而无法内联,并且相信您的编译器不会这样做。或者您可以选择另一种技术来提供代码 "weak linkage" 而无需向编译器建议内联函数:
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();
}
// ...
我使用了一个无偿的默认模板参数来为类型的成员函数提供弱链接,而无需声明成员函数 inline
。在 C++17 中,这个 可以正常工作 TM.
在 C++17 之前,您需要将 Polygon
重命名为 PolygonImp
之类的名称,然后使用声明提供此名称:
using Polygon = PolygonImp<>;
并且使用无偿模板技术,如果您想向编译器提示它们应该被内联,您仍然可以使用 inline
标记您的一些成员函数。
哪个最好取决于您的编译器。但此策略是您当前设计的 straight-forward 和机械扩展,不会增加费用,并且确实将您的界面与您的实现分开,这可能具有可读性优势。众所周知,真实世界的图书馆会使用这种技术。1, 2
有时可以在一行中声明和定义成员函数时做出折衷:
class Polygon
{
public:
// ...
int size() const {return size_;}
// ...
};