当前位置: 首页 > 知识库问答 >
问题:

在避免复制的同时将仿函数移入std::function

扈俊健
2023-03-14

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

#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上没有失败。堆栈跟踪提示执行的两个副本:

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

似乎在库内部,函子和参数在 invoke_ 的移动分配期间以交换方式复制。有两个问题:

    < li >为什么这是理想的行为,这种设计解决方案背后的动机是什么? < li >更新代码以达到与libstdc相同的语义的好方法是什么?

共有2个答案

习阳
2023-03-14

部分答案:

如果使用初始化列表初始化< code>invoke_:

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_的函子之间。

左丘涵畅
2023-03-14

libstdc和libc使用不同的小对象优化策略。

在libc中,如果下列条件成立,则可调用的存储在本地:

  • 可调用对象适合本地缓冲区;
  • 可调用对象是 nothrow copy constructible;和
  • 分配器是可构造的 nothrow 副本。

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

在 libstdc 中,如果以下条件成立,则可调用对象存储在本地:

  • 可调用对象适合本地缓冲区;
  • 可调用的是平凡的可复制类型。

(我在掩饰对齐的问题,因为它在这里不是特别相关。)

libstdc 选择何时使用小对象优化意味着,无论它在哪里适用,都可以通过复制为其提供存储的数组来复制可调用对象。但是代码中的函子参数类型不是普通的可复制的,因此 lambda 闭包类型也不是。lambda 的存储是行外分配的,拥有的可调用对象是从 lambda 移动构造的。此后永远不需要复制或移动它。

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

为什么libc不使用move构造函数?显然,这是一个错误。请参见LLVM错误33125,它是关于move构造函数的,而不是关于< code>swap函数的,但是同样的原则也适用。当源代码将可调用对象存储在小对象缓冲区时,< code>std::function的move构造函数使用copy构造函数,而不是move构造函数。原因是libc使用的类型擦除接口知道如何在忘记可调用对象的类型时处理可调用对象,该接口是一个名为< code>__base的类,它有复制可调用对象的虚函数和销毁可调用对象的虚函数,但没有移动可调用对象的虚函数——显然,添加移动支持会破坏ABI,所以在进一步通知之前不能这样做。

请注意,这只是一个错误,因为它具有次优性能。它不是不符合标准意义上的错误。libstdc 和 libc 都有有效的实现策略。该标准没有说明允许复制可调用对象多少次。

最好的办法是更新您的代码,使其正确性不依赖于复制可调用对象的次数。如果您确实无法承担复制的成本,但需要使用libc构建,还有一些其他策略,例如:

  • 填充可调用对象(通过捕获足够大小的虚拟对象),使其不适合小对象缓冲区
  • 使可调用的复制构造函数noexcept(false)(通过使用noexcept(false)复制构造函数捕获对象),使其不会进入小对象缓冲区;或
  • std::function中存储一个引用语义包装类,该类拥有实际可调用类型的std::shared_ptr和转发到实际可调用对象的operator()
 类似资料:
  • 我有以下函数用于从php json_encode中提取数据以在FullCalendar中使用。 我将对fullcalendar中的eventResize函数使用非常类似的代码,因此我想提取这一部分 它自己的功能(不是100%确定我在这里使用了正确的术语?)关于如何在全局范围内传递变量,我看到了这个答案jQuery-将函数中的变量传递给另一个函数,所以我尝试将上面的代码从eventDrop中移出,如

  • 我有一个严重的问题,我没有弄清楚。我有一个表名叫做“结果”。我想要实现的是,当我插入新记录时,它会正确地插入数据库。 在这个水平上,我的脚本运行良好。但是我想第二次点击添加新结果记录时,如果之前输入了数据,那么它将显示我的数据,如果我想更新我的数据,我可以。如果以前没有输入数据,那么我会将数据插入数据库。我成功地限制用户输入重复数据,但我没有成功地在同一页上显示数据。 我有一个显示测试页面,当我点

  • Scott Meyers发布了他的下一本书EC 11的内容和状态。他写道,书中的一项可以是“在函数签名中避免”。 如果可以用作函数参数、返回类型或类模板或函数模板参数,则可以有条件地从重载解析中删除函数或类。 在这个问题中,显示了所有三种解决方案。 作为功能参数: 作为模板参数: 作为返回类型: 哪种解决方案应该是首选的,为什么我要避开其他人? 在哪些情况下,函数签名中避免d::estnable_

  • 本文向大家介绍避免在MongoDB中重复输入?,包括了避免在MongoDB中重复输入?的使用技巧和注意事项,需要的朋友参考一下 为了避免在MongoDB中重复输入,可以使用。语法如下- 让我们实现以上语法。避免在MongoDB中重复条目的查询如下- 现在在上面的集合中插入一些记录。插入记录的查询如下- 每当您尝试再次插入相同记录时,都会出现此错误- 让我们插入另一条记录。查询如下- 在method

  • 问题内容: 我是mongodb的新手。我可以知道如何避免重复输入。在关系表中,我们使用主键来避免它。我可以知道如何使用Java在Mongodb中指定它吗? 问题答案: 在选项中使用索引。 您也可以跨多个字段执行此操作。 有关 更多详细信息和示例, 请参阅 文档中的 此部分 。 MongoDB索引可以有选择地施加一个 唯一的键约束 ,以确保不会插入任何索引键值与现有文档值匹配的文档。 如果希望从唯一

  • 我有一个非常简单的表,有3列,我需要一个尽可能轻量级的查询,只有当列有新值时才能插入。 如何编写sql查询来实现这一点?我在网站上已经看到了一些例子,但它们都被我无法理解的更复杂的查询(一些涉及子查询)所混淆。 似乎有不同的方法来做这件事,我需要找到最轻量级的一个,这样我就可以在循环中重复它,一次插入多个标签,而不会给服务器带来太大的压力。

  • Lodash castArray函数没有任何特殊之处。有没有什么方法可以在没有任何外部库的情况下,利用最新的语言功能解决这个问题,但时间很短? 如果您不熟悉该任务: 有没有办法在没有类型检查的情况下做到这一点?请注意,我寻找最短的等效物ES6。

  • 问题内容: 结果是 在哪里重复,因此哈希函数无法按预期工作。我将如何覆盖String数组的Hash方法。或就此而言,通用数组?有没有更好的方法来完成我要做的事情? 问题答案: 你不能 数组使用默认的基于身份的Object.hashCode()实现,无法覆盖它。不要在HashMap / HashSet中将数组用作键! 请改用一组列表。