拆分 header-only 库中的大文件

Splitting up large file in header-only library

我正在研究 header-only 库的代码库。它包含此 Polygon class,但问题是它相当大:大约 8000 行。我正试图打破它,但 运行 遇到了麻烦。此 class 和库的一些限制条件:

此 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 之外。这不是灾难性的,但几年后肯定很难理解。

我研究的备选方案:

您认为最好的解决方案是什么?你知道我可以尝试的任何替代方法吗?

我相信您可以使用 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_;}
    // ...
};