Как проверить для тривиально копируемых назначаемых лямбда

Я был бы признателен, если бы кто-нибудь мог дать мне подсказку о том, как проверить тривиальную способность к копированию функтора (предполагается, что используются lambdas). Как объяснено в this question, это реализация определяется, является ли лямбда тривиально скопируемой. Например, для кода, показанного в конце этого вопроса gcc (5.4) и msvc (2015), оба утверждения о пожаре, что они не могут быть тривиально копируемыми.

Я ожидал, что эти типы лямбда будут представлены struct с сохранением указателя this и с копией каждого зафиксированного значения (если есть). Таким образом, все они кажутся тривиально скопируемыми - по крайней мере, для случая, когда захваченные значения тривиально копируются.

Моим реальным вариантом использования этого вопроса является то, что я использую обратный вызов (минималистическая версия std::function, которая не выделяется и предназначена для очень простых функторов), которая хранит фиксированный буфер, в котором он копирует конструкции (на месте ) передал функтор. Затем я ожидал, что смогу копировать/назначать эти обратные вызовы, но для того, чтобы это работало (из коробки), простое копирование этих фиксированных буферов должно быть эквивалентно копированию/назначению функторов, содержащихся в них.

У меня есть два вопроса:

  • Представьте, что в test_functor() ниже я делаю new размещение, например. как

    new (&buffer) F(functor)

    Безопасно ли только memcopy этот буфер для вида lambdas, показанного ниже? Я ожидаю, что это должно быть так, так как для всех случаев имеется только this указатель захвачен или значения захвачены тривиально скопируемыми, но было бы неплохо, если бы кто-нибудь смог подтвердить это.

  • Как проверить, является ли простое копирование памяти, где хранится функтор, эквивалентно копированию функтора? Если ответ на первый вопрос положителен, то 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;
}

Ответы

Ответ 1

Нет, это не безопасно. Если компилятор говорит, что что-то не может быть тривиально скопировано, это не может быть.

Он может работать. Но это работает не означает, что это безопасно.

Даже если он работает сегодня, но завтра он перестает работать после обновления компилятора.

Исправление довольно простое. Напишите тип 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 Я думаю, хватит места для лямбда, хранящей два std::string s.

Накладные расходы, чтобы сделать все правильно, являются скромными (два указателя и небольшая косвенность). Вы можете снизить стоимость хранения до одного указателя на один экземпляр за счет большей косвенности (привяжите таблицу к "ручной vtable", хранящейся как статический локальный объект в функции factory: я могу предоставить ссылку на примеры, если это не так 't зажгите лампочку), но с помощью двух стираемых методов также можно сохранить их локально (при 3+ рассмотрите статическую таблицу), если пространство не стоит на высоте.

Вы уже вызываете "стирание", что в основном требует хранения указателя на функцию, добавление перемещения (и, возможно, копирование) и уничтожение - это не намного больше накладных расходов.