4

我担心这个网站上的某个地方会回答这样的问题,但我找不到它,因为我什至不知道如何提出这个问题。所以问题来了:

我有一个体素溺水功能。首先我计算偏移量、角度和东西,然后在我溺水之后。但是我为每个函数制作了几个版本,因为有时我想复制像素,有时是 blit,有时为每个像素复制 3*3 方格以获得平滑效果,有时如果调整对象大小,则只需将像素复制到屏幕上的 n*n 个像素。函数中心的那个小部分有很多版本。

我能做些什么而不是编写 10 个仅在代码的中心部分不同的相同函数?出于性能原因,将函数指针作为参数传递不是一种选择。我不确定让它们内联会起作用,因为我发送的参数不同:有时我计算体积(Z 值),有时我知道像素是从下到上绘制的。

我认为每个人都知道有某种方法可以在 C++ 中做这些事情。请告诉我我需要学习什么才能做到这一点。谢谢。

4

5 回答 5

6

传统的面向对象的方法是模板方法模式策略模式

模板法

第一个是文森佐的回答中描述的技术的扩展:不是编写简单的非虚拟包装器,而是编写一个包含整个算法的非虚拟函数。那些可能有所不同的部分是虚函数调用。给定实现所需的特定参数存储在提供该实现的派生类对象中。

例如。

class VoxelDrawer {
protected:
  virtual void copy(Coord from, Coord to) = 0;
  // any other functions you might want to change
public:
  virtual ~VoxelDrawer() {}
  void draw(arg) {
    for (;;) {
      // implement full algorithm
      copy(a,b);
    }
  }
};
class SmoothedVoxelDrawer: public VoxelDrawer {
  int radius; // algorithm-specific argument
  void copy(Coord from, Coord to) {
    blit(from.dx(-radius).dy(-radius),
         to.dx(-radius).dy(-radius),
         2*radius, 2*radius);
  }
public:
  SmoothedVoxelDrawer(int r) : radius(r) {}
};

战略

这很相似,但不是使用继承,而是将多态Copier对象作为参数传递给函数。它更加灵活,因为它将您的各种复制策略与特定功能解耦,您可以在其他功能中重复使用您的复制策略。

struct VoxelCopier {
  virtual void operator()(Coord from, Coord to) = 0;
};
struct SmoothedVoxelCopier: public VoxelCopier {
  // etc. as for SmoothedVoxelDrawer
};

void draw_voxels(arguments, VoxelCopier &copy) {
  for (;;) {
    // implement full algorithm
    copy(a,b);
  }
}

虽然比传入函数指针更整洁,但无论是模板方法还是策略都不太可能比仅传递函数指针有更好的性能:运行时多态仍然是间接函数调用。

政策

策略模式的现代 C++ 等价物是策略模式。这只是将运行时多态性替换为编译时多态性,以避免间接函数调用并启用内联

// you don't need a common base class for policies,
// since templates use duck typing
struct SmoothedVoxelCopier {
  int radius;
  void copy(Coord from, Coord to) { ... }
};
template <typename CopyPolicy>
void draw_voxels(arguments, CopyPolicy cp) {
  for (;;) {
    // implement full algorithm
    cp.copy(a,b);
  }
}

由于类型推导,您可以简单地调用

draw_voxels(arguments, SmoothedVoxelCopier(radius));
draw_voxels(arguments, OtherVoxelCopier(whatever));

注意。我在这里有点不一致:我曾经operator()让我的策略调用看起来像一个常规函数,但我的策略是一个正常的方法。只要你选择一个并坚持下去,这只是一个品味问题。

CRTP模板法

最后一种机制是模板方法的编译时多态性版本,它使用了奇怪的循环模板模式。

template <typename Impl>
class VoxelDrawerBase {
protected:
  Impl& impl() { return *static_cast<Impl*>(this); }

  void copy(Coord from, Coord to) {...}
  // *optional* default implementation, is *not* virtual
public:
  void draw(arg) {
    for (;;) {
      // implement full algorithm
      impl().copy(a,b);
    }
  }
};
class SmoothedVoxelDrawer: public VoxelDrawerBase<SmoothedVoxelDrawer> {
  int radius; // algorithm-specific argument
  void copy(Coord from, Coord to) {
    blit(from.dx(-radius).dy(-radius),
         to.dx(-radius).dy(-radius),
         2*radius, 2*radius);
  }
public:
  SmoothedVoxelDrawer(int r) : radius(r) {}
};

概括

一般来说,我更喜欢策略/策略模式,因为它们的耦合度较低且重用性更好,并且仅在您要参数化的顶级算法真正固定不变的情况下(即,当您进行重构时)才选择模板方法模式现有代码或确实确定您对变异点的分析)和重用确实不是问题。

如果有多个变化轴,使用模板方法也很痛苦(也就是说,您有多个方法,例如copy,并希望独立地改变它们的实现)。您最终会出现代码重复或混合继承。

于 2013-07-27T10:21:22.830 回答
3

我建议使用NVI成语。

你有你的公共方法,它调用一个私有函数,该函数实现了必须因情况而异的逻辑。

派生类必须提供该私有函数的实现,使它们专门用于其特定任务。

例子:

class A {
public:
    void do_base() {
        // [pre]
        specialized_do();
        // [post]
    }

private:
    virtual void specialized_do() = 0;
};

class B : public A {
private:
    void specialized_do() {
        // [implementation]
    }
};

优点是您可以在基类中保留一个通用实现,并根据任何子类的需要对其进行详细说明(只需要重新实现该specialized_do方法)。

缺点是您需要为每个实现使用不同的类型,但如果您的用例正在绘制不同的 UI 元素,那么这是要走的路。

于 2013-07-27T09:18:12.567 回答
1

您可以简单地使用策略模式

所以,而不是像

void do_something_one_way(...)
{
    //blah
    //blah
    //blah
    one_way();
    //blah
    //blah
}

void do_something_another_way(...)
{
    //blah
    //blah
    //blah
    another_way();
    //blah
    //blah
}

你将会有

void do_something(...)
{
    //blah
    //blah
    //blah
    any_which_way();
    //blah
    //blah
}

any_which_way可以是 lambda、仿函数、传入的策略类的虚拟成员函数。有很多选项。

你确定吗

“将函数指针作为参数传递不是一种选择”

真的会慢下来吗?

于 2013-07-27T09:40:49.820 回答
0

您可以使用模板方法模式策略模式。当您需要了解框架的内部结构以正确子类化一个类时,通常在白盒框架中使用模板方法模式。策略模式通常用在黑盒框架中,当你不应该知道框架的实现时,你只需要了解你应该实现的方法的契约。

出于性能原因,将函数指针作为参数传递不是一种选择。

你确定传递一个额外的参数会导致性能问题吗?在这种情况下,如果您使用 OOP 技术(如模板方法或策略),您可能会遇到类似的性能损失。但是通常需要使用 proflier 来确定性能下降的根源是什么。与复杂的算法相比,虚拟调用、传递附加参数、通过指针调用函数通常非常便宜。您可能会发现与其他代码相比,这些技术消耗的 CPU 资源的百分比微不足道。

我不确定让它们内联会起作用,因为我发送的参数不同:有时我计算体积(Z 值),有时我知道像素是从下到上绘制的。

您可以在所有情况下传递绘图所需的所有参数。或者,如果使用 Tempate 方法模式,则基类可以提供可以返回在不同情况下绘制所需数据的方法。在策略模式中,您可以将可以提供此类数据的对象实例传递给策略实现。

于 2013-07-27T10:48:59.453 回答
0

如果您的“中心部分”可以很好地参数化,您可以使用高阶函数。
这是一个简单的函数示例,它返回一个将 n 添加到其参数的函数:

#include <iostream>
#include<functional>
std::function<int(int)> n_adder(int n)
{
    return [=](int x){return x+n;};
}
int main()
{
    auto add_one = n_adder(1);
    std::cout<<add_one(5);
}
于 2013-07-27T09:48:45.197 回答