我担心这个网站上有这样的答案,但我找不到,因为我甚至不知道如何表达这个问题。所以问题来了:
我有一个体素drowing函数。首先,我计算偏移量、角度和其他东西,然后我做溺水。但我对每个函数都做了几个版本,因为有时我想复制像素,有时是blit,有时是为每个像素blit 3*3平方以获得平滑效果,有时只是将像素复制到屏幕上的n*n个像素(如果调整对象的大小)。一个函数中心的那个小部分有很多版本。
我能做些什么,而不是写10个相同的函数,它们只在代码的中心部分有所不同?出于性能原因,传递函数指针作为参数不是一种选择。我不确定让它们内联是否有效,因为我发送的参数不同:有时我计算体积(Z值),有时我知道像素是从下到上绘制的。
我想在C++中有一些方法可以做到这一点,每个人都知道。请告诉我做这件事需要学什么。谢谢
实现这一点的传统OO方法是模板方法模式和策略模式。
模板方法
第一个是Vincenzo的回答中描述的技术的扩展:你不写一个简单的非虚拟包装器,而是写一个包含整个算法的非虚拟函数。那些可能有所不同的部分是虚拟函数调用。给定实现所需的特定参数存储在提供该实现的派生类对象中。
例如。
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 ©) {
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模板方法
最后一种机制是模板方法的编译时多态性版本,它使用了Curioly Recurring模板模式。
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
,并且希望独立地改变它们的实现),那么使用模板方法也非常痛苦。您要么会导致代码重复,要么会导致混合继承。
我建议使用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元素,那么这就是方法。
您可以简单地使用策略模式
所以,不是像这样的东西
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、函子、传入的策略类的虚拟成员函数。有很多选项。
你确定
"传递函数指针作为参数不是一个选项";
它真的会减慢速度吗?
如果您的"中心部分"可以很好地参数化,那么您可以使用更高阶的函数
下面是一个简单的函数示例,它返回一个在参数中添加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);
}
您可以使用Template Method模式或Strategy模式。通常,当您需要了解框架的内部结构以正确地对类进行子类化时,在白盒框架中使用Template方法模式。策略模式通常用于黑匣子框架,当你不应该知道框架的实现时,因为你只需要了解你应该实现的方法的契约。
出于性能原因,传递函数指针作为参数不是一种选择。
您确定传递一个附加参数和会导致性能问题吗?在这种情况下,如果您使用OOP技术,如Template方法或Strategy,您可能会受到类似的性能惩罚。但通常有必要使用profilier来确定性能下降的原因。与复杂的算法相比,虚拟调用、传递附加参数、通过指针调用函数通常非常便宜。您可能会发现,与其他代码相比,这些技术消耗的CPU资源百分比微不足道。
我不确定让它们内联是否有效,因为我发送的参数不同:有时我计算体积(Z值),有时我知道像素是从下到上绘制的。
在任何情况下都可以传递绘图所需的所有参数。或者,如果使用Tempate方法模式,基类可以提供方法,这些方法可以返回在不同情况下绘制所需的数据。在Strategy模式中,您可以将可以提供此类数据的对象的实例传递给Strategy实现。