【问题标题】:Why doesn't GCC's std::function use rvalue references to arguments passed by value to pass them between its internal delegates?为什么 GCC 的 std::function 不使用对按值传递的参数的右值引用在其内部委托之间传递它们?
【发布时间】:2014-12-20 00:22:22
【问题描述】:

首先,考虑以下代码:

#include <iostream>
#include <functional>

struct Noisy
{
  Noisy() { std::cout << "Noisy()" << std::endl; }
  Noisy(const Noisy&) { std::cout << "Noisy(const Noisy&)" << std::endl; }
  Noisy(Noisy&&) { std::cout << "Noisy(Noisy&&)" << std::endl; }
  ~Noisy() { std::cout << "~Noisy()" << std::endl; }
};

void foo(Noisy n)
{
  std::cout << "foo(Noisy)" << std::endl;
}

int main()
{
  Noisy n;
  std::function<void(Noisy)> f = foo;
  f(n);
}

及其在不同编译器中的输出:

Visual C++ (see live)

Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()

Clang (libc++) (see live)

Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()

GCC 4.9.0 (see live)

Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
~Noisy()

也就是说,与 Visual C++(和 Clang+libc++)相比,GCC 执行一个移动/复制操作,让我们同意,并不是在所有情况下都有效(例如 std::array&lt;double, 1000&gt; 参数)。

据我了解,std::function 需要对一些包含实际函数对象的内部包装器进行虚拟调用(在我的情况下为 foo)。因此,使用转发引用完美转发是不可能的(因为虚拟成员函数不能被模板化)。

但是,我可以想象实现可以在内部 std::forward 内部所有参数,无论它们是通过值传递还是通过引用传递,如下所示:

// interface for callable objects with given signature
template <class Ret, class... Args>
struct function_impl<Ret(Args...)> {
    virtual Ret call(Args&&... args) = 0; // rvalues or collaped lvalues
};

// clever function container
template <class Ret, class... Args>
struct function<Ret(Args...)> {
    // ...
    Ret operator()(Args... args) { // by value, like in the signature
        return impl->call(std::forward<Args>(args)...); // but forward them, why not?
    }

    function_impl<Ret(Args...)>* impl;
};

// wrapper for raw function pointers
template <class Ret, class... Args>
struct function_wrapper<Ret(Args...)> : function_impl<Ret(Args...)> {
    // ...
    Ret (*f)(Args...);

    virtual Ret call(Args&&... args) override { // see && next to Args!
        return f(std::forward<Args>(args)...);
    }
};

因为按值传递的参数只会变成右值引用(很好,为什么不呢?),右值引用将折叠并保留为右值引用,而左值引用将折叠并保留为左值引用(see this proposal live)。这避免了任意数量的内部助手/委托之间的复制/移动。

所以我的问题是,为什么 GCC 对按值传递的参数执行额外的复制/移动操作,而 Visual C++(或 Clang+libc++)却没有(因为它似乎没有必要)?我希望 STL 的设计/实现能够获得最佳性能。

请注意,在std::function 签名中使用右值引用,如std::function&lt;void(Noisy&amp;&amp;)&gt;,对我来说不是解决方案。


请注意,我并不是在寻求解决方法。我认为这两种可能的解决方法都不正确。

a) 使用 const 左值引用!

为什么不呢?因为现在当我使用右值调用 f 时:

std::function<void(const Noisy&)> f = foo;
f(Noisy{});

它暂时禁止Noisy移动操作并强制复制。

b) 然后使用非常量右值引用!

为什么不呢?因为现在当我使用左值调用 f 时:

Noisy n;
std::function<void(Noisy&&)> f = foo;
f(n);

它根本不编译。

【问题讨论】:

  • 您的问题是:“为什么 GCC 对按值传递的参数执行额外的复制/移动操作?”,在那里得到了回答。还是我错过了什么?
  • @BЈовић 看起来另一个答案解释了为什么有 2 个副本,而这个答案问为什么 gcc 有 3 个。
  • 我相信在 gcc 的 bugzilla 中提交增强 PR 是可行的方法。他们可能选择不改变它。 operator() 不直接调用该函数,而是将其委托给一个助手_M_invoker,因此是额外的副本。
  • 我认为原因可能是 GCC 的std::function 是基于我们的std::tr1::function,它是在右值引用存在多年之前编写的。我已经优化了其他呼叫包装器,例如 std::bindstd::threadstd::async 以使用完美转发,但 function 可能仍然不是最理想的。我去看看。

标签: c++ c++11 gcc libstdc++ std-function


【解决方案1】:

在 libstdc++ 中,std::function::operator() 不直接调用该函数,它将该任务委托给帮助程序 _M_invoker。这种额外的间接级别解释了额外的副本。我没有研究代码,所以我不知道这个助手是否只是为了方便,或者它是否发挥了强大的作用。无论如何,我相信要走的路是在gcc's bugzilla 提交增强 PR。

【讨论】:

  • 我不确定这是否真的是一个答案,但是 BЈовић 让我发布它,除了代码是这样写的,没有人之外,对于“为什么”可能没有任何真正的答案之前投诉过。
  • 总之感谢您的回答。但是在我看来,任何额外的间接级别都可以对右值引用进行操作,就像在我的 sn-p 中一样。当一个参数按值传递时:void operator()(Noisy n),然后调用std::forward&lt;Noisy&gt;(n),然后将其转换为右值引用,以便其他帮助程序可以将其operator() 声明为operator()(Args&amp;&amp;...),以便Noisy 参数变为@987654330 @,然后额外转发到另一个间接级别也可以使用上述技巧。这最大限度地减少了副本的数量(我认为是这样)。
  • @MarcAndreson 也许确实可以。然后可以选择是删除间接更简单还是让间接更聪明。这就是您可以在错误报告中提供的所有信息。除非你想自己贡献一个补丁?这将是受欢迎的:gcc.gnu.org/wiki/GettingStarted .
【解决方案2】:

对于clang-3.4.2和libstdc++-4.9.1,它是:

Noisy()
Noisy(const Noisy&)
Noisy(const Noisy&)
Noisy(const Noisy&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
~Noisy()

根本没有右值引用!

【讨论】:

    【解决方案3】:

    这是一个糟糕的 QoI 案例。真的没有什么好的理由。

    有一个复杂的工作。将类型擦除的类型传递给T 的构造而不执行它。然后调用里面的构造。有了省略,就没有那么棘手了。

    template<class T>
    struct ctor_view {
      ctor_view(T&&t):ctor_view(tag{},std::move(t)){}
      ctor_view(T const&t):ctor_view(tag{},t){}
      ctor_view():ptr(nullptr),
        f(+[](void*)->T{return{};})
      {}
      T operator()()const&&{
        return f(ptr);
      };
      operator T()const&&{
        return std::move(*this)();
      }
    private:
      void* ptr;
      T(*f)(void*);
      struct tag {};
      template<class U,class pU=std::decay_t<U>*>
      ctor_view(tag, U&&t):ptr(const_cast<pU>(std::addressof(t))),
        f(+[](void* p){
          U&& t = static_cast<U&&>(*(pU)(p));
          return std::forward<U>(t);
        })
      {}
    };
    

    上面可能有错误,但上面需要 T&amp;&amp;T const&amp; 或什么都没有,并为 T 生成一个类型擦除的工厂,移动或复制构造它。

    std::function< void(ctor_view<X>) > = [](X x){};
    

    然后将避免额外的移动。该签名的大多数(但不是全部)使用都应该有效(除了某些返回类型扣除的情况)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-24
      • 2016-10-22
      • 2020-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多