如何正确使用通过转发参考传递的可调用项?

时间:2019-04-21 21:10:40

标签: c++ language-lawyer c++17 decltype perfect-forwarding

我习惯于将lambda函数(和其他可调用对象)传递给模板函数-并按如下所述使用它们

template <typename F>
auto foo (F && f)
 {
   // ...

   auto x = std::forward<F>(f)(/* some arguments */);

   // ... 
 }

我的意思是:我习惯于将它们通过转发引用传递,并称为通过std::forward传递。

另一个Stack Overflow用户辩称(请参见this answer的评论),这一次调用函数两次或两次以上是很危险的,因为它在语义上是无效的,并且在调用该函数时可能存在危险(甚至还有未定义的行为)带有r值参考。

我部分地误解了他的意思(我的错),但是仍然存在的疑问是,以下bar()函数(在同一对象上具有不可数倍的std::forward)是正确的代码还是(也许只是潜在的危险)。

template <typename F>
auto bar (F && f)
 {
   using A = typename decltype(std::function{std::forward<F>(f)})::result_type;

   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

3 个答案:

答案 0 :(得分:4)

前进只是有条件的举动。

因此,通常来说,多次转发同一事物就像多次转发一样危险。

未评估的前锋不会动弹,因此不会计数。

遍历std::function会增加一些麻烦:推论仅适用于函数指针和具有&&不合格的单个函数调用运算符的函数对象。对于这些,如果两个都编译,则右值和左值调用总是等效的。

答案 1 :(得分:2)

在这种情况下,我要说一般规则。在将变量从中移出/转发后,您不应该对其进行任何处理,除非对其进行赋值。

因此...

  

如何正确使用通过转发参考传递的可调用项?

仅当您确定不会再被调用时(即,如果有话,在上次通话中),才转发。

如果从未调用过一次,则没有理由不转发。


关于您的摘要为何如此危险的原因,请考虑使用仿函数:

template <typename T>
struct foo
{
    T value;
    const T &operator()() const & {return value;}
    T &&operator()() && {return std::move(value);}
};

作为一种优化,operator()在调用右值时允许调用者从value移出。

现在,如果给定此函子,则模板将无法编译(因为如T.C.所说,std::function在这种情况下将无法确定返回类型)。

但是如果我们稍作改动:

template <typename A, typename F>
auto bar (F && f)
 {    
   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

那么当给定该函子时,它将break spectacularly

答案 2 :(得分:1)

如果您只是将可调用对象转发到另一个地方,或者只是将可调用对象仅调用一次,那么我认为使用std::forward通常是正确的选择。如here所述,这将保留可调用对象的值类别,并允许调用潜在调用函数的“正确”版本。

原始线程中的问题是可调用对象在循环中被调用,因此有可能被多次调用。另一个线程的具体示例是

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(f(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(f(element));

    return sequence;
}

在这里,我相信调用std::forward<F>(f)(element)而不是f(element),即

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(std::forward<F>(f)(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(std::forward<F>(f)(element));

    return sequence;
}

可能会有问题。据我所知,右值的定义特征是不能明确引用它。特别是,自然不可能在一个表达式中多次使用相同的prvalue(至少我想不到一个)。此外,据我所知,如果您使用std::movestd::forward或其他任何方法来获取xvalue,即使在相同的原始对象上,每次的结果都会是一个新的xvalue 。因此,也不可能有一种方法可以多次引用相同的xvalue。由于相同的右值不能被多次使用,因此我会争辩(另请参见this answer下的注释),对于重载的函数调用运算符执行只能在一次情况下执行一次的操作通常是有效的做法呼叫发生在右值,例如:

class MyFancyCallable
{
public:
    void operator () & { /* do some stuff */ }
    void operator () && { /* do some stuff in a special way that can only be done once */ }
};

MyFancyCallable的实现可以假定将采用&&限定版本的调用不可能(在给定对象上)多次发生。因此,我会考虑将同一个可调用对象转发到多个调用中,这样在语义上会被破坏。

当然,从技术上讲,对于转发或移动对象的实际含义没有统一定义。最后,这实际上取决于所涉及的特定类型的实现以在其中分配含义。因此,您只需在接口的一部分中指定传递给算法的潜在可调用对象必须能够处理在引用同一对象的右值上多次被调用的情况。但是,这样做几乎违反了在C ++中通常使用右值引用机制的所有约定,而且我真的不知道这样做会带来什么好处……