0

我正在研究仅标头库的代码库。它包含这个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类之外。这不是灾难性的,但在一年后肯定很难理解。

我研究的替代方案:

  • 混合。但是,mixin 无法访问基类中的数据。
  • 自由浮动函数类似于 Boost 的做法。然而,这有几个问题: 自由浮动函数无法访问受保护的字段。当自由浮动函数需要它时,这些文件需要相互包含导致Polygon类是不完整的类型。需要提供指向多边形的指针(不确定这是否会得到优化?)。
  • 提供实现类的模板参数。这最终类似于自由浮动函数,因为实现类需要访问 的受保护字段,PolygonPolygon实现需要它时它是不完整的,并且Polygon仍然需要以某种方式提供给实现。
  • 我曾想过通过继承来实现这一点,其中受保护的数据成员位于私有基类中。子类是详细的实现。然后会有一个包含所有公共函数的公共类,它们仍然可以调用详细实现。然而,这是典型的钻石问题,需要虚拟桌子。不过没有对此进行测试,因为这很难设置。

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

4

2 回答 2

2

我相信您可以使用奇怪重复的模板模式(也称为静态多态)。这是一篇关于为什么它不是未定义行为的好帖子为什么CRTP 定义行为中的沮丧

--

我用一条线稍微简化了你的例子。这是基类,在本例中是长度计算函数的实现:

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} {}
};
于 2019-09-17T01:28:46.360 回答
1

我不认为你这样做对你的读者有任何好处:

private:
//Private implementations separated out to different files for readability.
#include "detail/polygon_area.hpp"
#include "detail/polygon_contains.hpp"
...

现在你的读者必须打开另一个文件才能看到幕后发生的事情,但仍然没有Polygon.

我推荐的第一件事是一个简单的重构,它只是在声明外而不是在声明内定义所有现有的成员函数:

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) { /*...*/ }

这对功能或效率的影响为零,但有助于将接口与实现分开时的可读性。它也不会通过打开虚假的“实现头文件”来惩罚编译时间。打开文件是编译器可以做的更昂贵的事情之一。

现在这已经完成,进入一个更微妙的点: inline只是一个提示,你的编译器可以自由地接受或不接受这个提示。这里inline主要用于将您的函数标记为具有“弱链接”,以便当此标头包含在多个源中时,您的链接器不会抱怨重复定义。

所以你可以坚持这个设计,它可能会“暗示”将一些函数标记为inline真正大到内联的函数,并相信你的编译器不会。或者您可以选择另一种技术来为您的代码提供“弱链接”,而无需向编译器建议内联函数:

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并且使用免费模板技术,如果您想向编译器提示它们应该被内联,您仍然可以标记您的一些成员函数。

哪个最好取决于您的编译器。但是这种策略是您当前设计的直接和机械扩展,不会增加费用,并且确实将您的接口与您的实现分开,这可以具有可读性优势。众所周知,现实世界的图书馆使用这种技术。1 , 2

当成员函数可以在一行中声明和定义时,有时会做出妥协:

class Polygon
{
public:
    // ...
    int size() const {return size_;}
    // ...
};
于 2019-09-17T02:47:17.473 回答