首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >将函子移到std::function的同时避免复制

将函子移到std::function的同时避免复制
EN

Stack Overflow用户
提问于 2022-05-21 19:44:38
回答 2查看 117关注 0票数 1

我正试图将一个函子移动到一个对象内的lambda中,如下所示:

代码语言:javascript
复制
#include <functional>
#include <iostream>

#include "boost/stacktrace.hpp"

#define fwd(o) std::forward<decltype(o)>(o)

struct CopyCounter {
  CopyCounter() noexcept = default;
  CopyCounter(const CopyCounter &) noexcept {
    std::cout << "Copied at" << boost::stacktrace::stacktrace() << std::endl;
    counter++;
  }
  CopyCounter(CopyCounter &&) noexcept = default;

  CopyCounter &operator=(CopyCounter &&) noexcept = default;
  CopyCounter &operator=(const CopyCounter &) noexcept {
    std::cout << "Copied at " << boost::stacktrace::stacktrace() << std::endl;
    counter++;
    return *this;
  }

  inline static size_t counter = 0;
};

struct Argument : CopyCounter {};

struct Functor : CopyCounter {
  int operator()(Argument) { return 42; }
};

template <class Result>
class Invoker {
  std::function<void()> invoke_;
  Result* result_ = nullptr;

  template <class Functor, class... Args>
  Invoker(Functor&& f, Args&&... args) {
    if constexpr (std::is_same_v<Result, void>) {
      invoke_ = [this, f = fwd(f), ... args = fwd(args)]() mutable {
        f(fwd(args)...);
      };
    } else {
      invoke_ = [this, f = fwd(f), ...args = fwd(args)]() mutable { 
        result_ = new Result(f(fwd(args)...));
      };
    }
  }
  template <class Functor, class... Args>
  friend auto make_invoker(Functor&& f, Args&&... args);

public:
  ~Invoker() {
    if (result_) delete result_;
  }
};

template <class Functor, class... Args>
auto make_invoker(Functor&& f, Args&&... args) {
  return Invoker<decltype(f(args...))>(fwd(f), fwd(args)...);
}

int main() {
  Functor f;
  Argument a;
  auto i = make_invoker(std::move(f), std::move(a));
  assert(CopyCounter::counter == 0);
  return 0;
}

有些令人惊讶的是,最后一个断言在libc++上失败了,但libstdc++失败了。堆栈跟踪提示执行的两个副本:

代码语言:javascript
复制
Copied at  0# CopyCounter at /usr/include/boost/stacktrace/stacktrace.hpp:?
 1# 0x00000000004C812E at ./src/csc_cpp/move_functors.cpp:38
 2# std::__1::__function::__value_func<void ()>::swap(std::__1::__function::__value_func<void ()>&) at /usr/lib/llvm-10/bin/../include/c++/v1/functional:?
 3# ~__value_func at /usr/lib/llvm-10/bin/../include/c++/v1/functional:1825
 4# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 5# _start in ./bin/./src/csc_cpp/move_functors

Copied at  0# CopyCounter at /usr/include/boost/stacktrace/stacktrace.hpp:?
 1# std::__1::__function::__value_func<void ()>::swap(std::__1::__function::__value_func<void ()>&) at /usr/lib/llvm-10/bin/../include/c++/v1/functional:?
 2# ~__value_func at /usr/lib/llvm-10/bin/../include/c++/v1/functional:1825
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./bin/./src/csc_cpp/move_functors

似乎在库中,函子和参数在swap中被复制,在invoke_的移动分配过程中。有两个问题:

  1. 为什么这是我们想要的行为?这个设计解决方案背后的动机是什么?
  2. 更新代码以达到与libstdc++相同的语义的好方法是什么?
EN

回答 2

Stack Overflow用户

发布于 2022-05-22 01:14:02

libstdc++和libc++采用不同的小对象优化策略.

在libc++中,如果存在以下情况,则可调用的存储在本地:

  • 可调用的配合本地缓冲器;
  • 可调用的副本是不可构造的;以及
  • 分配程序不能抛出可构造的复制。

(注意: C++17中的分配器支持已从标准中删除。然而,实际上,在不破坏现有代码的情况下,它不能从标准库实现中删除,至少在很长一段时间内是如此。)

在libstdc++中,如果存在以下情况,则可调用的存储在本地:

  • 可调用的配合本地缓冲器;
  • 可调用的类型为微不足道的可复制类型。

(我正在讨论对齐问题,因为它在这里并不特别重要。)

libstdc++对何时使用小对象优化的选择意味着,无论何时应用,都可以通过复制为其提供存储空间的数组来复制可调用的对象。但是代码中的FunctorArgument类型并不是微不足道的可复制类型,因此lambda闭包类型也是不可复制的。lambda的存储是从lambda构建的--行外存储,而拥有可调用的存储是移动的。此后,它永远不需要被复制或移动。

另一方面,libc++将您的可调用存储在小对象缓冲区中。在分配给invoke_期间,它必须用默认构造的std::function交换包含小对象缓冲区中的闭包对象的std::function。这意味着可调用必须从一个std::function实例的小对象缓冲区移动到另一个实例的小对象缓冲区。libc++通过复制可调用的,然后销毁副本的源来完成此操作。

为什么libc++不使用移动构造函数?很明显是个窃听器。请参阅LLVM bug 33125,它是关于move构造函数的,而不是swap函数,但适用相同的原则。当源将可调用的可调用函数存储在小对象缓冲区中时,std::function的移动构造函数使用复制构造函数,而不是移动构造函数。原因是libc++使用类型擦除接口知道如何在忘记其类型的同时处理可调用,它是一个名为__base的类,它具有用于复制可调用的虚拟函数和用于销毁可调用的虚拟函数,但是没有任何可调用的函数--显然,添加移动支持会破坏ABI,因此在进一步的通知之前是无法完成的。

请注意,这只是一个错误,因为它的性能不佳。从某种意义上说,这不是一个不符合标准的错误。libstdc++和libc++都有有效的实现策略。标准没有说明允许复制可调用的次数。

最好的做法是更新您的代码,使其正确性不取决于可调用的复制次数。如果您真的无法承担副本的成本,但需要使用libc++进行构建,那么还有其他一些策略,如:

  • 填充可调用对象(通过捕获足够大小的虚拟对象),使其不适合小对象缓冲区;
  • 使可调用的复制构造函数noexcept(false) (通过使用noexcept(false)复制构造函数捕获对象),使其不会进入小对象;或
  • std::function中存储一个引用语义包装类,它拥有实际可调用的std::shared_ptr和转发到实际可调用对象的operator()
票数 3
EN

Stack Overflow用户

发布于 2022-05-21 20:13:42

部分答复:

如果使用初始化列表初始化invoke_

代码语言:javascript
复制
class Invoker {
  std::function<void()> invoke_;
  int result_;

public:
  template <class Functor, class... Args>
  Invoker(Functor&& f, Args&&... args) :
    invoke_([this, f = fwd(f), ...args = fwd(args)]() mutable { 
      result_ = f(fwd(args)...);
    }) { }
};

断言不败。所以,我猜这个交换是在默认初始化的invoke_和指定给invoke_的函子之间的。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72332743

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档