问题 C ++模板元编程功夫挑战(取代宏函数定义)


情况

我想实现Composite模式:

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_fun 和 std::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));
}

2689
2017-07-22 05:52


起源

下面的仿函数解决了更难的问题,仿函数存储参数,并且从容器中调用每个对象,然后可以使用任何参数调用每个对象的任何方法。您可以根据需要构建任意数量的仿函数,它们很简单。 - stefanB


答案:


我不确定我是否真的看到了这个问题。为什么不是这样的:

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的


10
2017-07-22 06:11



std::bind2nd对我来说很新鲜。如果我有3个参数怎么办? - kizzx2
我认为这里的循环迭代是一个特定的情况,我对糟糕的例子不好。让我们想象一下,我们有一个 LoggedCompositeAnimal 记录所有操作 之前 和 后 迭代(“准备召唤3只动物奔跑......”,“完成对3只动物的呼唤。花费时间50秒”)。 - kizzx2
@ kizzx2:实际上是一个极限。 :)这部分标准库充满了糟糕的设计。如果你想认真对待绑定的东西,请查看 Boost.Bind。它有一个更简单,更富有表现力和强大的界面。 (否则,你必须手动制作仿函数。)在你的第二个评论中,除了制作可重复使用的代码之外,没有太多可做的事情。我将添加一个示例,看看我们是否在同一页面上。 - GManNickG
然后只使用boost :: bind - vividos
@GMan:加入了一些严肃的功夫!这非常接近理想的解决方案(我将研究boost :: bind)。我想“然后改变 for_each“部分将驻留在 LoggedCompositeAnimal.cpp 并且不会改变 for_each 对于其他人,对吧? (所有这些让我想知道“只使用宏”在这种情况下是否是“正确的”解决方案) - kizzx2


您可以使用仿函数而不是方法:

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!

2
2017-07-22 06:27



感谢Functors的信息!但是,这种方法的一个潜在问题是它打破了复合模式。我的来电者不应该知道 Animal 我给的实际上是一个 Composite。所以我的来电者不应该知道打电话 apply,他应该打电话 Eat 和 Run 直。 - kizzx2
我注意到了,因此一个小修复:) - stefanB
你可以添加'template <class T> void operator - > *(T t){apply(t); }and then even write CA - > *运行(); CA - > *吃( “布丁”);`:-) - Nordic Mainframe
仔细观察,我不确定Functor方法如何解决原始前提(DRY)。因为我需要为每个创建新方法,然后只委托给Functor,它的唯一功能是提供功能 Run 和 Eat,为什么我不在里面写函数体 Run 和 Eat 但是必须委托给一个仿函数? - kizzx2
这取决于你的设计。你可以提供 apply 和算子。那么唯一的区别就是复合类 apply 将它应用于所有对象。您可以通过添加新仿函数来添加新功能。 - stefanB