如何测试普通复制可赋值的lambdas

How to test for trivially copy assignable lambdas

本文关键字:复制 赋值 lambdas 何测试 测试      更新时间:2023-10-16

如果有人能给我一个关于如何测试一个函子的微不足道的可复制性的提示,我将不胜感激(lambdas意味着要使用)。正如在这个问题中所解释的,lambda是否可复制是由实现定义的。例如,对于这个问题末尾显示的代码,gcc(5.4)和msvc(2015)都发出断言,这些不是简单的复制可分配的。

我期望这些类型的lambda由struct表示,保持this指针,并具有每个捕获值的副本(如果有的话)。因此,它们看起来都是非常可复制的——至少对于捕获值非常可复制的情况来说是这样。

我真正的用例驱动这个问题是,我正在使用一个回调(std::function的极简版本,不分配,意味着非常简单的函子),保持一个固定的缓冲区,它复制构造(在适当的地方)传递函子。然后我期望能够复制/赋值这些回调函数,但为了使其工作(开箱即用),这些固定缓冲区的简单内存复制应该等同于复制/赋值保存在其中的函子。

我有两个问题:

  1. 想象在test_functor()下面我做new的位置,例如

    new (&buffer) F(functor)

    对于下面所示的那种lambdas,仅仅memcopy这个缓冲区是安全的吗?我希望这应该是这样的,因为在所有情况下,只有this指针被捕获或捕获的值是微不足道的可复制的,但如果有人能确认这一点,那就太好了。

  2. 如何测试简单复制保存函子的内存是否等同于复制函子?如果第一个问题的答案是肯定的,那么std::is_trivially_copy_assignable不是正确的答案。

#include <type_traits>
template <typename F>
void test_functor(const F& functor)
{
    static_assert(std::is_trivially_destructible<F>::value,
                  "Functor not trivially destructible");
    static_assert(std::is_trivially_copy_constructible<F>::value,
                  "Functor not trivially copy constructible");
    static_assert(std::is_trivially_copy_assignable<F>::value,
                  "Functor not trivially copy assignable");
}
struct A
{
    void test() { test_functor([this]() { }); }
};
struct B
{
    void test() { test_functor([this](int v) { value = v; }); }
    int value;
};
struct C
{
    void test(int v) { test_functor([=]() { value = v; }); }
    int value;
};
int main()
{
    A a;
    B b;
    C c;
    a.test();
    b.test();
    c.test(1);
    return 0;
}

不,不安全。如果编译器说某些东西不能简单地复制,它就不能被复制。

它可能工作。但它工作并不意味着它是安全的。

即使它今天工作,但明天它停止工作后,编译器更新。

修复非常简单。写一个SBO类型(小缓冲区优化),不需要简单的复制。

template<std::size_t S, std::size_t A>
struct SBO {
  void(*destroy)(SBO*) = nullptr;
//  void(*copy_ctor)(SBO const* src, SBO* dest) = nullptr;
  void(*move_ctor)(SBO* src, SBO* dest) = nullptr;
  std::aligned_storage_t< S, A > buffer;
  void clear() {
    auto d = destroy;
    destroy = nullptr;
  //  copy_ctor = nullptr;
    move_ctor = nullptr;
    if (d) d(this);
  }
  template<class T, class...Args>
  T* emplace( Args&&... args ) {
    static_assert( sizeof(T) <= S && alignof(T) <= A, "not enough space or alignment" );
    T* r = new( (void*)&buffer ) T(std::forward<Args>(args)...);
    destroy = [](SBO* buffer) {
      ((T*)&buffer->buffer)->~T();
    };
    // do you need a copy ctor?  If not, don't include this:
    //copy_ctor = [](SBO const* src, SBO* dest) {
    //  auto s = (T const*)&src.buffer;
    //  dest->clear();
    //  dest->emplace<T>( *s );
    //};
    move_ctor = [](SBO* src, SBO* dest) {
      auto* s = (T*)&src->buffer;
      dest->clear();
      dest->emplace<T>( std::move(*s) );
      src->clear();
    };
    return r;
  }
  SBO() = default;
  SBO(SBO&& o) {
    if (o.move_ctor) {
      o.move_ctor(&o, this);
    }
  }
  SBO& operator=(SBO&& o) {
    if (this == &o) return *this; // self assign clear, which seems surprising
    if (o.move_ctor) {
      o.move_ctor(&o, this);
    }
    return *this;
  }
  // do you need a copy ctor?  If so, implement `SBO const&` ctor/assign
};

生活例子。

现在是笑点。std::function几乎可以肯定已经为您做了

std::function中放置一个具有无抛出移动和构造的小类型,并询问创建是否可以抛出。我猜你的实现将使用SBO来存储类型。

MSVC 2015我认为有足够的空间为lambda存储两个std::string s。

做正确事情的开销是适度的(两个指针和一点间接)。你可以将存储成本降低到每个实例一个指针,但代价是更间接(将表粘贴到一个"手动虚函数表"中,作为静态本地存储在工厂函数中:如果这还不亮起的话,我可以提供一个示例链接),但是如果有2个擦除方法,则不妨将它们存储在本地(考虑静态表的3+),除非空间有限。

您已经"擦除"调用,这基本上需要存储一个函数指针,添加移动(可能是复制)和销毁并没有那么多的开销。