Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test for trivially copy assignable lambdas

Tags:

c++

c++11

lambda

I'd appreciate if anybody could give me a hint on how to test for trivial copy-ability of a functor (lambdas are meant to be used). As explained in this question it is implementation defined whether lambda is trivially copyable. For example for the code shown at the end of this question gcc (5.4) and msvc (2015) both fire assertions that these are not trivially copy assignable.

I expected these types of lambdas to be represented by struct keeping this pointer and having copy of every captured value (if any). So they all appear to be trivially copyable - at least for the case when the captured values are trivially copyable.

My real use case driving this question is that I'm using a callback (minimalistic version of std::function that does not allocate and is meant for very simple functors) that keeps a fixed buffer in which it copy constructs (in place) passed functor. Then I expected to be able to copy/assign these callbacks but in order for this to work (out of the box) simple mem-copying of these fixed buffers should be equivalent to coping/assigning of the functors kept in them.

So I have two questions:

  1. Imagine that in test_functor() below I do new placement e.g. like

    new (&buffer) F(functor)

    Is it safe to just memcopy this buffer for the kind of lambdas shown below? I expect this should be so since for all cases there is only this pointer captured or values captured are trivially copyable, but it would be nice if somebody could confirm this.

  2. How can I test whether simple copying of memory where functor is kept is equivalent to copying of the functor? If the answer for the first question is positive then std::is_trivially_copy_assignable not the right answer.

#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;
}
like image 464
AndrzejO Avatar asked Aug 25 '16 11:08

AndrzejO


1 Answers

No it is not safe. If the compiler says something cannot be trivially copied, it cannot be.

It might work. But it working doesn't mean it is safe.

Even if it works today, but tomorrow it stops working after a the compiler updates.

The fix is pretty simple. Write a SBO type (small buffer optimization) that doesn't require trivially copiable.

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
};

live example.

Now here is the punchline. std::function almost certainly already does this for you.

Put a small type with a no-throw move and construct in a std::function and ask if the creation could throw. I'm guessing your implementation will use a SBO to store the type in there.

MSVC 2015 I think has enough room for a lambda storing two std::strings.

The overhead to do things right is modest (two pointers, and a little indirection). You can drop the storage cost down to one pointer per instance at the cost of more indirection (stick the table into a "manual vtable" stored as a static local in a factory function: I can provide a link to examples if that doesn't light a bulb), but with 2 erased methods might as well store them locally (at 3+ consider the static table) unless space is at a premium.

You are already "erasing" invocation, which basically requires storing a function pointer, adding move (and maybe copy) and destroy isn't that much more overhead.

like image 109
Yakk - Adam Nevraumont Avatar answered Sep 26 '22 19:09

Yakk - Adam Nevraumont