【问题标题】:lambda functors assignment workaroundlambda 函子分配解决方法
【发布时间】:2013-07-11 09:18:19
【问题描述】:

下面的代码有问题吗?

#include <iostream>

#include <type_traits>

template <typename T>
void assign_lambda(T&& f)
{
  typedef typename std::remove_reference<T>::type functor_type;

  typedef typename std::aligned_storage<sizeof(functor_type),
    std::alignment_of<functor_type>::value>::type buffer_type;

  static char store[sizeof(buffer_type)];

  auto const p(new (store) functor_type(std::forward<T>(f)));

  (*p)();
}

int main()
{
  for (int i(0); i != 5; ++i)
  {
    assign_lambda([i](){ std::cout << i << std::endl; });
  }

  return 0;
}

我担心这样做可能不标准和/或危险。

编辑: 为什么要初始化为 char 数组?如果该块足够大,则可以从堆中分配一个大小为sizeof(buffer_type) 的块并重复使用以进行重复分配(即避免重复的内存分配)。

void*operator new(std::size_t size);

效果:由 new 表达式 (5.3.4) 调用的分配函数 (3.7.4.1) 分配 size 字节的存储空间,适当对齐以表示该大小的任何对象。

我想如果我从堆中分配,对齐问题就会消失。

【问题讨论】:

  • 在多线程环境下效果不佳
  • @bobah 没有什么是互斥锁无法修复的。
  • std::function 有什么问题?
  • 一旦您按如下所述修复对齐方式,此特定代码就可以工作。但是,如果你的 lambda 捕获了任何需要析构函数的东西,你就不会调用它,所以你会得到泄漏。
  • @user1095108 你不需要std::function。只需存储一个静态的functor_type 而不是放置新的。

标签: c++ c++11


【解决方案1】:

您必须确保storefunctor_type 正确对齐。除此之外,我没有看到任何关于标准一致性的问题。但是,您可以通过使数组非静态来轻松解决多线程问题,因为sizeof 提供了一个编译时常量。

§5.3.4,14要求对齐:

[ 注意:当分配函数返回一个非空值时,它必须是一个指向已为对象保留空间的存储块的指针。假定存储块已适当对齐并具有请求的大小。 [...] - 尾注]

还有一段关于对齐的第 3.7.4.1 节,但该段落明确不适用于放置新(第 18.6.1.3,1 节)。

要正确对齐,您可以执行以下操作:

template <typename T>
void assign_lambda(T&& f)
{
  typedef typename std::remove_reference<T>::type functor_type;

  //alignas(functor_type) char store[sizeof(functor_type)];
  std::aligned_storage<sizeof(functor_type), 
            std::alignment_of<functor_type>::value>::type store;

  auto const p(new (&store) functor_type(std::forward<T>(f)));

  (*p)();

  //"placement delete"
  p->~functor_type();
}

更新: 上面显示的方法与仅使用普通变量没有什么不同:

template <typename T>
void assign_lambda(T&& f)
{
  typedef typename std::remove_reference<T>::type functor_type;

  functor_type func{std::forward<T>(f)};

  func();
}

如果它 必须 是函数内的静态变量,则您将需要一个 RAII 包装器用于不可分配的仿函数。仅仅placement-newing 是不够的,因为函子不会被正确销毁,并且它们拥有的资源(例如通过捕获的智能指针)不会被释放。

template <typename F>
struct RAIIFunctor {
  typedef typename std::remove_reference<F>::type functor_type;

  std::aligned_storage<sizeof(functor_type), 
            std::alignment_of<functor_type>::value>::type store;

  functor_type* f;

  RAIIFunctor() : f{nullptr} {}
  ~RAIIFunctor() { destroy(); }

  template <class T>
  void assign(T&& t) {
    destroy();
    f = new(&store) functor_type {std::forward<T>(t)};
  }

  void destroy() {
    if (f) 
      f->~functor_type();
    f = nullptr;
  }

  void operator() {
    (*f)();
  }
};


template <typename T>
void assign_lambda(T&& f)
{
  static RAIIFunctor<T> func;

  func.assign(std::forward<T>(f));
  func();
}

你可以看到代码在运行here

【讨论】:

  • 如果没有正确对齐会怎样?能否请您引用标准中的内容?
  • 如果您没有提供正确的对齐方式,则行为未定义。在实践中,它的范围从 x86 上的“如果稍微慢一点就按预期工作”到其他架构上的“总线错误”。
  • @user1095108 很可能......不行。如果你非要问:肯定不是
  • 因为对齐、强制转换、新位置等都是非常高级的语言功能。你最好不要问这个问题。更具体地说,@R.MartinhoFernandes 所说的......
  • @user1095108 修复了那个。
【解决方案2】:

我不明白。为什么使用aligned_storage 只是为了获得一些大小来创建未初始化的存储,而不是......使用它提供的对齐存储?这几乎就像从柏林到里斯本一样,先搭乘柏林 -> 里斯本航班,然后搭乘里斯本 -> 莫斯科航班。

  typedef typename std::remove_reference<T>::type functor_type;

  typedef typename std::aligned_storage<sizeof(functor_type),
    std::alignment_of<functor_type>::value>::type buffer_type;

  static buffer_type store;

  auto const p(new (&store) functor_type(std::forward<T>(f)));

【讨论】:

  • 感谢您将我指向aligned_storage - 直到现在才知道alignofalignas
  • 我对位置 new 的理解是,new 可能不会返回指向给定块开头的指针,但可能会向前移动一点以满足对齐要求,因此有时您需要比您分配的对象大小更多的存储空间。此外,我最关心的是我的代码是否正常,是否符合标准。你的也可以,我明白,但是我的代码,比如它,也可以吗?
  • 不,placement new 对指针没有影响(因此您的代码不能保证满足对齐要求)。
【解决方案3】:

除了已经提到的对齐问题,您正在通过放置 new 创建 lambda 的副本,但您并没有销毁副本。

以下代码说明了问题:

// This class plays the role of the OP's lambdas
struct Probe {
    Probe() { std::cout << "Ctr" << '\n'; }
    Probe(const Probe&) { std::cout << "Cpy-ctr" << '\n'; }
    ~Probe() { std::cout << "Dtr" << '\n'; }

};

// This plays the role of the OP's assign_lambda
void f(const Probe& p) {

    typedef typename std::aligned_storage<sizeof(Probe),
        std::alignment_of<Probe>::value>::type buffer_type;

    static buffer_type store;
    new (&store) Probe(p);
}

int main() {

    Probe p;

    // This plays the role of the loop
    f(p);
    f(p);
    f(p);
}

输出是:

Ctr
Cpy-ctr
Cpy-ctr
Cpy-ctr
Dtr

因此,构造了 4 个对象,而只有一个对象被销毁。

此外,在 OP 的代码中,storestatic,这意味着一个 lambda 在另一个之上重复构建,就好像后者只是原始内存一样。

【讨论】:

  • static 对象具有应用程序的生命周期,静态 lambda 的析构函数也只调用一次。我在这里尝试将 lambda 复制到静态“lambda”中。
  • @user1095108:您没有将 lambda b 分配给 lambda a。您正在使用a 占用的内存,就像它是原始内存一样,并在其上创建b 的副本。对象a 未正确销毁。基本上,您试图在同一个内存地址中创建两个不同的对象,并且(因为一个不是另一个的子对象)这是不允许的。
  • 在每次分配之前调用自定义删除器能否解决这个问题?
  • @user1095108 仍然不会破坏最后分配的仿函数。如果 lambda 通过值捕获某个 RAII 对象,则不会适当地释放资源。
  • @user1095108 我更新了我的答案以在assign_lambda 中存储一个静态的、RAII 管理的仿函数
猜你喜欢
  • 2018-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-18
  • 2016-06-02
  • 2023-04-09
相关资源
最近更新 更多