C++模板元编程技巧挑战(替换宏函数定义)

11

情况

我想要实现组合模式:

class Animal
{
public:
    virtual void Run() = 0;
    virtual void Eat(const std::string & food) = 0;
    virtual ~Animal(){}
};

class Human : public Animal
{
public:
    void Run(){ std::cout << "Hey Guys I'm Running!" << std::endl; }
    void Eat(const std::string & food)
    {
        std::cout << "I am eating " << food << "; Yummy!" << std::endl;
    }
};

class Horse : public Animal
{
public:
    void Run(){ std::cout << "I am running real fast!" << std::endl; }
    void Eat(const std::string & food)
    {
        std::cout << "Meah!! " << food << ", Meah!!" << std::endl;
    }
};

class CompositeAnimal : public Animal
{
public:
    void Run()
    {
        for(std::vector<Animal *>::iterator i = animals.begin();
            i != animals.end(); ++i)
        {
            (*i)->Run();
        }
    }

    // It's not DRY. yuck!
    void Eat(const std::string & food)
    {
        for(std::vector<Animal *>::iterator i = animals.begin();
            i != animals.end(); ++i)
        {
            (*i)->Eat(food);
        }
    }

    void Add(Animal * animal)
    {
        animals.push_back(animal);
    }

private:
    std::vector<Animal *> animals;
};

问题

你看,对于我简单的组合模式需求,最终我还是要编写很多重复的代码来迭代同一个数组。

使用宏的可能解决方案

#define COMPOSITE_ANIMAL_DELEGATE(_methodName, _paramArgs, _callArgs)\
    void _methodName _paramArgs                                      \
    {                                                                \
        for(std::vector<Animal *>::iterator i = animals.begin();     \
            i != animals.end(); ++i)                                 \
        {                                                            \
            (*i)->_methodName _callArgs;                             \
        }                                                            \
    }

现在我可以这样使用它:

class CompositeAnimal : public Animal
{
public:
    // It "seems" DRY. Cool

    COMPOSITE_ANIMAL_DELEGATE(Run, (), ())
    COMPOSITE_ANIMAL_DELEGATE(Eat, (const std::string & food), (food))

    void Add(Animal * animal)
    {
        animals.push_back(animal);
    }

private:
    std::vector<Animal *> animals
};

问题

有没有一种使用C++元编程更加简洁的方式来实现它?

更难的问题

std::for_each被建议作为一种解决方案。我认为我们的问题是更一般性问题的一个特例,让我们考虑我们新的宏:

#define LOGGED_COMPOSITE_ANIMAL_DELEGATE(_methodName, _paramArgs, _callArgs)\
    void _methodName _paramArgs                                      \
    {                                                                \
        log << "Iterating over " << animals.size() << " animals";    \
        for(std::vector<Animal *>::iterator i = animals.begin();     \
            i != animals.end(); ++i)                                 \
        {                                                            \
            (*i)->_methodName _callArgs;                             \
        }                                                            \
        log << "Done"                                                \
    }

看起来这个不能被for_each替换。

后续影响

通过查看GMan的出色答案,可以确定C++中的这部分内容确实不容易。就我个人而言,如果我们只是想减少样板代码的数量,我认为宏可能是这种特定情况下正确的工具。

GMan建议使用std::mem_funstd::bind2nd返回函数对象。不幸的是,该API不支持3个参数(我简直不敢相信这样的东西会发布到STL中)。

为说明目的,这里的委托函数使用boost::bind代替:

void Run()
{
    for_each(boost::bind(&Animal::Run, _1));
}

void Eat(const std::string & food)
{
    for_each(boost::bind(&Animal::Eat, _1, food));
}

更难的问题可以通过下面的函数解决,函数对象存储参数,并在容器中调用每个对象时被调用,它可以使用任何参数调用每个对象的任何方法。您可以构造尽可能多的函数对象,它们非常简单。 - stefanB
2个回答

10

我不确定我真正看到问题所在。为什么不尝试这样做:

void Run()
{
    std::for_each(animals.begin(), animals.end(),
                    std::mem_fun(&Animal::Run));
}

void Eat(const std::string & food)
{
    std::for_each(animals.begin(), animals.end(),
                    std::bind2nd(std::mem_fun(&Animal::Eat), food));
}

还不错。


如果你真的想要摆脱(少量的)样板代码,可以添加:

template <typename Func>
void for_each(Func func)
{
    std::for_each(animals.begin(), animals.end(), func);
}
作为私有实用成员,则使用它:
void Run()
{
    for_each(std::mem_fun(&Animal::Run));
}

void Eat(const std::string & food)
{
    for_each(std::bind2nd(std::mem_fun(&Animal::Eat), food));
}

更为简洁,无需元编程。

实际上,元编程最终会失败。你试图生成函数,而函数是以文本方式定义的。元编程无法生成文本,因此你必须在某个地方使用宏来生成文本。

在下一个级别上,你可以编写函数,然后尝试去掉样板代码。 std::for_each 做得很好。当然,如已经演示的那样,如果你发现那还是太重复了,就将其分解出来。


针对评论中的 LoggedCompositeAnimal 示例,您最好创建类似于:

class log_action
{
public:
    // could also take the stream to output to
    log_action(const std::string& pMessage) :
    mMessage(pMessage),
    mTime(std::clock())
    {
        std::cout << "Ready to call " << pMessage << std::endl;
    }

    ~log_action(void)
    {
        const std::clock_t endTime = std::clock();

        std::cout << "Done calling " << pMessage << std::endl;
        std::cout << "Spent time: " << ((endTime - mTime) / CLOCKS_PER_SEC)
                    << " seconds." << std::endl;
    }

private:
    std::string mMessage;
    std::clock_t mTime;
};

这只是大部分自动记录操作。然后:

class LoggedCompositeAnimal : public CompositeAnimal
{
public:
    void Run()
    {
        log_action log(compose_message("Run"));
        CompositeAnimal::Run();
    }

    void Eat(const std::string & food)
    {
        log_action log(compose_message("Eat"));
        CompositeAnimal::Eat(food);
    }

private:
    const std::string compose_message(const std::string& pAction)
    {
        return pAction + " on " +
                    lexical_cast<std::string>(animals.size()) + " animals.";
    }
};

就像那样。 lexical_cast的信息。


std::bind2nd 对我来说还很新。如果我有三个参数怎么办? - kizzx2
@GMan:这个加法真是高深莫测!这几乎是一个理想的解决方案(我会研究boost::bind)。我想,“然后改变for_each”部分将驻留在LoggedCompositeAnimal.cpp中,并不会为其他人改变for_each,对吗? (所有这些让我想知道,在这种情况下,“只使用宏”是否是“正确”的解决方案) - kizzx2
@kizz:抱歉,我一直将LoggedCompositeAnimal视为我们唯一的组合类。我已经进行了编辑,以反映我如何使用CompositeAnimal作为基础实现它。 - GManNickG
@dribeas: 你能具体说明如何"实际改进代码"吗?是的,保持DRY并不总是更好,但大多数情况下是这样的(好的,口水战开始了!:p)。以我们的示例为例,有些情况下Composite的每个函数都不应该盲目地委托给子组件。但在情况下,这正是我需要的,所以我正在寻找更聪明的方法来做到这一点。当然,你可以说语言是设计用于处理通用情况,并且并不存在每个特殊情况的最佳解决方案。这纯粹是为了教育目的。 - kizzx2
1
抛开这些,我同意DRY并不总是更好的选择——我从过度架构的经历中吃了苦头。 "简化架构"比"应用更多架构"更难以想象。 - kizzx2
显示剩余5条评论

2
您可以使用函数对象而不是方法:
struct Run
{
    void operator()(Animal * a)
    {
        a->Run();
    }
};

struct Eat
{
    std::string food;
    Eat(const std::string& food) : food(food) {}

    void operator()(Animal * a)
    {
        a->Eat(food);
    }
};

并添加CompositeAnimal :: apply#include <algorithm>):

template <typename Func>
void apply(Func& f)
{
    std::for_each(animals.begin(), animals.end(), f);
}

那么您的代码将会像这样运行:
int main()
{
    CompositeAnimal ca;
    ca.Add(new Horse());
    ca.Add(new Human());

    Run r;
    ca.apply(r);

    Eat e("dinner");
    ca.apply(e);
}

输出:

> ./x
I am running real fast!
Hey Guys I'm Running!
Meah!! dinner, Meah!!
I am eating dinner; Yummy!

为了保持界面一致性,您可以再进一步。
将结构体 Run 重命名为 Running,将结构体 Eat 重命名为 Eating,以防止方法/结构体冲突。
然后,CompositeAnimal::Run 将会像这样,使用 apply 方法和 struct Running:
void Run()
{
    Running r;
    apply(r);
}

同样地,CompositeAnimal::Eat 也是如此:

void Eat(const std::string & food)
{
    Eating e(food);
    apply(e);
}

现在您可以进行呼叫:

ca.Run();
ca.Eat("dinner");

输出结果仍然相同:

I am running real fast!
Hey Guys I'm Running!
Meah!! dinner, Meah!!
I am eating dinner; Yummy!

谢谢关于Functors的信息!然而,这种方法可能会导致一个潜在的问题,即它破坏了组合模式。我的调用者不应该知道我传递的Animal实际上是一个Composite。所以我的调用者不需要知道要调用apply,他只需要直接调用EatRun - kizzx2
我注意到了,因此进行了小修复 :) - stefanB
你可以添加 template<class T> void operator->*(T t) { apply(t); },然后甚至可以写成 ca->*Run(); ca->*Eat("pudding"); :-) - Nordic Mainframe
仔细看,我不确定Functor方法如何解决原始前提(DRY)。由于我需要为每个方法创建新的方法,然后只需委托给Functor,其唯一功能是提供“Run”和“Eat”的功能,为什么不直接在“Run”和“Eat”中编写函数体而必须委托给一个functor呢? - kizzx2
这取决于你的设计。你可以只提供“apply”和函数对象。然后,唯一的区别就是在Compound类上,“apply”会将其应用到所有对象上。通过添加新函数对象,你可以添加新的功能。 - stefanB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接