如何正确使用通过转发引用传递的callable?

问题描述 投票:2回答:3

我习惯将lambda函数(和其他callables)传递给模板函数 - 并使用它们 - 如下所示

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;
 }
c++ language-lawyer c++17 decltype perfect-forwarding
3个回答
2
投票

我会说一般规则适用于这种情况。在移动/转发变量后,您不应该对变量执行任何操作,除非可以分配给变量。

从而...

如何正确使用通过转发引用传递的callable?

只有在您确定不会再次呼叫时(即在最后一次呼叫时,如果有的话),只能前进。

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


至于为什么你的代码片段可能有危险,请考虑以下仿函数:

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

作为优化,operator()在rvalue上调用时允许调用者从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给这个仿函数。


4
投票

前进只是一个有条件的举动。

因此,一般来说,多次转发相同的东西与多次移动一样危险。

未评估的前锋不会移动任何东西,所以这些都不算数。

通过std::function进行路由增加了一个皱纹:该推导仅适用于函数指针和函数对象,其中单个函数调用运算符不是&&限定的。对于这些,如果两者都编译,则rvalue和左值调用总是等效的。


1
投票

如果你要将调用者转发到另一个地方或者只是简单地调用callable一次,我会争辩说使用std::forward是正确的事情。正如here所解释的那样,这将保留可调用的值类别,并允许调用可能重载的函数调用操作符的“正确”版本。

原始线程中的问题是在循环中调用了callable,因此可能会多次调用。另一个帖子的具体例子是

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。由于相同的rvalue不能多次使用,我认为(参见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的实现可能假设选择&&限定版本的调用不可能发生多次(在给定对象上)。因此,我会考虑将同一个callable转发到多个语句中进行语义分解。

当然,从技术上讲,对于转发或移动物体实际意味着什么并没有通用的定义。最后,实际上由所涉及的特定类型的实现来赋予其意义。因此,您可以简单地指定作为接口的一部分,传递给您的算法的潜在callables必须能够处理在引用同一对象的rvalue上多次调用。但是,这样做几乎违背了关于如何在C ++中使用右值引用机制的所有约定,我真的没有看到可能会从这样做中获得什么......

© www.soinside.com 2019 - 2024. All rights reserved.