2014-04-27 23 views
2

这是this question的后续行动,我鼓励我在可变模板运算符()(...)实现中使用完美转发。这里,我想用调用free函数和成员函数具有可变参数我的观察者模式:完美的转发导致错误我不明白

#ifndef _SIGNALS_H_ 
#define _SIGNALS_H_ 

#include <utility> 

/** Interface for delegates with a specific set of arguments **/ 
template<typename... args> 
class AbstractDelegate 
{ 
    public: 
    virtual void operator()(args&&...) const = 0; 
    virtual ~AbstractDelegate() {} 
}; 

/** Concrete function delegate that discards the function's return value **/ 
template<typename ReturnType, typename... args> 
class FnDelegate : public AbstractDelegate<args...> 
{ 
    public: 
    /** member function typedef **/ 
    using Fn = ReturnType(*)(args...); 

    /** constructor **/ 
    FnDelegate(Fn fn) 
     : fn_{fn} 
    { 
    } 

    /** call operator that calls the stored function **/ 
    void operator()(args&&... a) const override 
    { 
     (*fn_)(std::forward<args>(a)...); 
    } 

    private: 
    /** function pointer **/ 
    const Fn fn_; 
}; 

/** forward declaration **/ 
template<typename... args> 
class Connection; 

/** Signal class that can be connected to**/ 
template<typename... args> 
class Signal 
{ 
    public: 
    /** connection pointer typedef **/ 
    typedef Connection<args...>* connection_p; 

    /** constructor **/ 
    Signal() 
     : connections_(NULL), 
     blocked_(false) 
     { 
     } 

    /** call operator that notifes all connections associated with this Signal. 
     The most recently associated connection will be notified first **/ 
    void operator()(args&&... a) const 
    { 
     // only notify connections if this signal is not blocked 
     if (!blocked()) 
     { 
     auto c = connections_; 
     while(c != NULL) 
     { 
      (*c)(std::forward<args>(a)...); 
      c = c->next(); 
     } 
     } 
    } 

    /** connect to this signal **/ 
    void connect(connection_p p) 
    { 
     p->next_ = connections_; 
     connections_ = p; 
     p->signal_ = this; 
    } 

    /** disconnect from this signal. 
     Invalidates the connection's signal pointer 
     and removes the connection from the list **/ 
    void disconnect(connection_p conn) 
    { 
     // find connection and remove it from the list 
     connection_p c = connections_; 
     if (c == conn) 
     { 
     connections_ = connections_->next(); 
     conn->next_ = NULL; 
     conn->signal_ = NULL; 
     return; 
     } 
     while(c != NULL) 
     { 
     if (c->next() == conn) 
     { 
      c->next_ = conn->next(); 
      conn->next_ = NULL; 
      conn->signal_ = NULL; 
      return; 
     } 
     c = c->next(); 
     } 
    } 

    /** block events from this signal **/ 
    void block() 
    { 
     blocked_ = true; 
    } 

    /** unblock events from this signal **/ 
    void unblock() 
    { 
     blocked_ = false; 
    } 

    /** is this signal blocked? **/ 
    bool blocked() const 
    { 
     return blocked_; 
    } 

    /** destructor. disconnects all connections **/ 
    ~Signal() 
    { 
     connection_p p = connections_; 
     while(p != NULL) 
     { 
     connection_p n = p->next(); 
     disconnect(p); 
     p = n; 
     } 
    } 

    connection_p connections() const {return connections_;} 

    private: 
    connection_p connections_; 
    bool blocked_; 
}; 

/** connection class that can be connected to a signal **/ 
template<typename... args> 
class Connection 
{ 
    public: 
    /** template constructor for static member functions and free functions. 
     allocates a new delegate on the heap **/ 
    template<typename ReturnType> 
    Connection(Signal<args...>& signal, ReturnType (*Fn)(args...)) 
     : delegate_(new FnDelegate<ReturnType, args...>(Fn)), 
     signal_(NULL), 
     next_(NULL), 
     blocked_(false) 
    { 
     signal.connect(this); 
    } 

    /** get reference to this connection's delegate **/ 
    AbstractDelegate<args...>& delegate() const 
    { 
     return *delegate_; 
    } 

    /** call this connection's delegate if not blocked **/ 
    void operator()(args&&... a) const 
    { 
     if (!blocked()) 
     { 
     delegate()(std::forward<args>(a)...); 
     } 
    } 

    /** get pointer to next connection in the signal's list **/ 
    Connection* next() const 
    { 
     return next_; 
    } 

    /** is this connection connected to a valid signal? **/ 
    bool connected() const 
    { 
     return (signal_ != NULL); 
    } 

    /** block events for this connection **/ 
    void block() 
    { 
     blocked_ = true; 
    } 

    /** unblock events for this connection **/ 
    void unblock() 
    { 
     blocked_ = false; 
    } 

    /** is this connection blocked? **/ 
    bool blocked() const 
    { 
     return blocked_; 
    } 

    /** desctructor. If the signal is still alive, disconnects from it **/ 
    ~Connection() 
    { 
     if (signal_ != NULL) 
     { 
     signal_->disconnect(this); 
     } 
     delete delegate_; 
    } 

    const Signal<args...>* signal() const {return signal_;} 

    friend class Signal<args...>; 
    private: 
    AbstractDelegate<args...>* delegate_; 
    Signal<args...>* signal_; 
    Connection* next_; 
    bool blocked_; 
}; 

/** free connect function: creates a connection (static member or free function) on the heap 
    that can be used anonymously **/ 
template<typename ReturnType, typename... args> 
Connection<args...>* connect(Signal<args...>& signal, ReturnType (*fn)(args...)) 
{ 
    return new Connection<args...>(signal, fn); 
} 

#endif // _SIGNALS_H_ 

我试图使用它在这些方面:

Signal<int> sig; 

void print(int i) 
{ 
    std::cout << "print(" << i << ")" << std::endl; 
} 

int get(int i) 
{ 
    return i; 
} 

int main() 
{ 
    connect(sig, print); 
    sig(3); 
    int i = 4; 
    sig(i); // <-- here I get an error 
    sig(get(5)); 
} 

我得到的错误是

main.cpp: In function ‘int main()’: 
main.cpp:21:10: error: cannot bind ‘int’ lvalue to ‘int&&’ 
sig(i); 
    ^
In file included from main.cpp:2:0: 
main.h:89:10: error: initializing argument 1 of ‘void Signal<args>::operator()(args&& ...) const [with args = {int}]’ 
void operator()(args&&... a) const 
    ^

的误差消失,当我用const int&无处不在,即Signal<const int&> sigvoid print(const int&),但我不明白为什么。此外,在“标志”信号的情况下,通过const bool&会感到尴尬。

你能提出一个解决方案,可以在这里提供更多的灵活性吗?

+1

由于它直接基于@ Morwenn在原始问题中的回答,因此我在那里评论过。 –

+5

你的'sig :: operator()'不使用完美转发。为了完美的转发工作,您需要模板类型推导,这意味着您有一个函数模板(其中类型可以从参数表达式中推导出来)。 'operator()'不是函数模板。 – dyp

+0

如果您删除了与此问题无关的所有内容(比如您未调用的函数),那么您的代码将更容易遵循。 – celtschk

回答

7

您的代码审查失败。通用参考技术并非天真地适用于抽象接口。

请勿在签名中使用Args&&,使用纯虚拟接口的Args...。在实现内部,当且仅当参数类型是一个&&右值引用或文字时,使用参数std::forward<Args>(args)...才有条件地移动arg的最后一次(或只有一次)。

当您使用Args&&...时,您的函数采用左值引用或左值引用。在许多情况下,您都不希望明确地将参数类型传入。在类型演绎上下文中,Args&&...将自动检测您的参数类型的“最佳”匹配,但不会推导出这些类型。

当您在类型推导的上下文中使用Args...时,推导的类型始终为文字。通常这是次优的。但是,如果您指定类型,则没有问题。使用std::forward有条件地移动变量。如果传入的类型是右值引用或文字,它会移动。这恰好在完美转发通用引用时以及我在上面描述的用例中做正确的事情。

template<typename... args> 
class AbstractDelegate 
{ 
    public: 
    virtual void operator()(args...) const = 0; 
    virtual ~AbstractDelegate() {} 
}; 

/** Concrete function delegate that discards the function's return value **/ 
template<typename ReturnType, typename... args> 
class FnDelegate : public AbstractDelegate<args...> 
{ 
    public: 
    /** member function typedef **/ 
    using Fn = ReturnType(*)(args...); 

    /** constructor **/ 
    FnDelegate(Fn fn) 
     : fn_{fn} 
    { 
    } 

    /** call operator that calls the stored function **/ 
    void operator()(args... a) const override 
    { 
     (*fn_)(std::forward<args>(a)...); // last (&only) use of each arg, ok to forward 
    } 

...

Signal,你可以删除forward(保持&&,因为它是一个非virtual接口),或者你可以在最后一个信号做更复杂的东西,只有forward

// stay args&&... here: 
void operator()(args&&... a) const { 
    // only notify connections if this signal is not blocked 
    if (!blocked()) 
    { 
    auto c = connections_; 
    while(c) 
    { 
     auto c_next = c->next(); 
     if (c_next) 
     (*c)(a...); 
     else 
     (*c)(std::forward<args>(a)...); // last use, can forward 
     c = c_next; 
    } 
    } 
} 

由于forward是有条件的举动,在大多数情况下,你应该只用它在上下文其中move将是有效的。 (当然不是,由于移动的条件性质,但该模式是有效的)。

由于变量不止一次来自变量move,因此它不止一次从变量生效到forward

+0

这看起来很有帮助,但我不确定如何将您的解释转化为实际的代码。虽然你在'AbstractDelegate'的纯虚拟接口中使用'args ...'的建议很清楚。 “最后一次我使用arg”将在'FnDelegate'的operator()中,对吗? – Christoph

+0

@Christoph coles注意代码修改包括在内。 – Yakk

+0

在关于'Signal :: operator()'的提示中,你说我可以将右值引用给参数列表:void operator()(args && ... a)'。这是我得到编译器错误的地方,那么最后一次使用的转发如何提供帮助?即使转发只允许上次使用,代码仍然无法编译,除非我删除'&&'。 – Christoph

2

从编译器的眼睛通过步行此,代码的以下简化提取物是根本问题:

template <typename... Args>    // (a) 
struct Signal 
{ 
    void operator()(Args&&... a) const; // (b) 
}; 

int main() 
{ 
    Signal<int> sig;      // (c) 
    int i = 4;       // (d) 
    sig(i);        // (e) 
} 

线(c)实例Signal<int>,其经由线(A,B)具有一个方法void operator()(int&& a) const;。行(d)实例化类型为int的命名变量。行(e)尝试将该int传递给需要类型为int&&的值的方法。编译器正确地抱怨这一点。

在这一点上,你应该已经能够解释为什么用const int&替换你的模板类型没有同样的问题:因为没有Args&&扩展,所以没有声明r值引用。对常量值的常规引用按照您的预期工作。

纠正报告错误的一种方法是将int转换为int&&;这种类型的转换允许将其传递给被调用函数来使其内容无效。为了使它脱颖而出的代码,这种操作是通过std::move(i)完成:

sig(std::move(i));     // (e') 

虽然这解决了编译器错误,这是不是真的,你想要什么。 Yakk关于完美转发和虚拟函数继承之间交互的评论实际上是对模板函数和继承的评论。这是正确的,除非像Yakk指出的那样,Signal<Args>不是一个继承类。 (它在AbstractDelegate类树中变得很重要,它在继承类中,所有这些问题都会重复出现,但我们现在暂且搁置一下。)您真正想要的仅仅是省略在行(b)中的&&,并放弃使用std::forward<>()

void operator()(Args... a) const;  // (b') 

而这就是你现在应该停下来的地方。一旦您准备好深入了解代码审查的建议出了什么问题,请继续。


为了解这些问题,下一步是研究为什么可以使用完美转发。要理解这一点,您还必须了解移动语义,这又意味着了解r-values and r-value references。这里有很多很好的文献,但是这个摘要版本如下。

在一些场景中,类似于这里的场景,一个函数试图将相同的接口作为另一个函数公开,或许加上或减去一个参数。在C++ 03中,您必须至少知道参数的数量,以便您可以创建足够的模板参数以匹配它们。在C++ 11中,可以使用可变参数模板来自动匹配参数的数量和类型。但是,C++ 11还引入了移动值的概念。移动一个值通常比构建一个副本更有效,所以支持这一点是有益的。函数通过接受r值引用来接受移动的值,例如int&&。 (在实践中int是一个坏榜样,因为它是微不足道的复制; std::vector<int>将是一个更相关的例子,但我会坚持int为简单起见。)

然而,r值引用是怪异。当您声明参数int&&时,函数中的局部变量具有不同的类型:plaint int。所以即使是一个接受移动变量的函数参数(一个参数为r型值参数 - T&&)的变量声明了一个本地的l值并且不能被隐式移动。完美转发就是知道参数是一个r值参考的概念,并使用这些信息反过来将参数的值移到它所调用的函数中。从句法上看,这看起来像std::forward<int&&>(i),但是因为如果您知道它应该被移动,那么您将其编写为std::move(i),因此std::forward仅在模板函数中非常有用。因此,它看起来更像std::forward<T>(i),或者在可变数据包的情况下,std::forward<Args>(a)...

您的代码应该使用完美转发吗?我可能会说不。要做出决定,首先必须确定代码的这一部分是否或者可能是性能至关重要的。如果您没有编写通用库,并且没有其他说明的配置文件数据,则应该假定它不是性能至关重要的。其次,Yakk关于模板化虚拟功能的观点又回来了;你不能覆盖一个不同类型的虚拟函数,所以这个模板对你的AbstractDelegate类提供的类型擦除级别没有用。既然你不能在那里完美地转发,那么在调用它的层面上转发就没有多少益处。编译器优化甚至可以有效地忽略外层,直接进入虚函数调用。

假设你忽略这个建议,并决定你想要完美的转发。要获得完美的转发,您需要位于编译器执行模板参数推演的位置。对于您将需要更换线(b)同样:

template <typename... OpArgs> 
    void operator()(OpArgs&&... a) const; // (b'') 

这种方式与OpArgs组中的每个推断值将反映传递给方法的参数的L值或R值的性质。您在线(e)的呼叫将生成一个功能void Signal<Args>::operator()(int a) const;,该功能将接受它通过的int。这在Signal类中很好,但会导致覆盖AbstractDelegate的继承类型中的虚函数的问题。为什么?因为AbstractDelegate<Args>只会创建一个要覆盖的虚拟函数,但operator()的每个不同实例都可能需要不同的虚拟函数。这与原来写入的行(b)不存在问题,因为模板类型是在类级别指定的,因此只有一个函数说明。但是这阻碍了完美转发的能力。

根据aschepler的评论,使用std::function<void(args...)>代替您的自定义AbstractDelegate类树简化了您的代码,但不会更改上述内容。你会注意到,std::function does not perfectly forward

这就是为什么我建议不要在这里完美转发的麻烦。首先是一只红鲱鱼,在这个特殊情况下,它导致你走上了一条不利的道路。

+0

谢谢你这个非常有用的答案。现在我吃了蓝色的药丸,但我会回来探索一下兔子洞。 – Christoph

+0

这是一个全面的答案。这真的很有用:) – Morwenn

+0

通过“不打扰完美的转发在这里”,你的意思是删除所有右值引用和使用“std :: forward”?这确实有效,并且可能不会导致我需要使用类中的问题。 – Christoph