Передача некопируемого объекта закрытия в параметр std::function

В C++14 лямбда-выражение может захватывать переменные, перемещаясь от них с помощью инициализаторов захвата. Однако это делает результирующий объект замыкания некопируемым. Если у меня есть существующая функция, которая принимает аргумент std::function (который я не могу изменить), я не могу передать объект замыкания, потому что конструктор std::function требует, чтобы данный функтор был CopyConstructible.

#include <iostream>
#include <memory>

void doit(std::function<void()> f) {
    f();
}

int main()
{
    std::unique_ptr<int> p(new int(5));
    doit([p = std::move(p)] () { std::cout << *p << std::endl; });
}

Это дает следующие ошибки:

/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:1911:10: error: 
      call to implicitly-deleted copy constructor of '<lambda at test.cpp:10:7>'
            new _Functor(*__source._M_access<_Functor*>());
                ^        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:1946:8: note: in
      instantiation of member function 'std::_Function_base::_Base_manager<<lambda at test.cpp:10:7>
      >::_M_clone' requested here
              _M_clone(__dest, __source, _Local_storage());
              ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:2457:33: note: in
      instantiation of member function 'std::_Function_base::_Base_manager<<lambda at test.cpp:10:7>
      >::_M_manager' requested here
            _M_manager = &_My_handler::_M_manager;
                                       ^
test.cpp:10:7: note: in instantiation of function template specialization 'std::function<void
      ()>::function<<lambda at test.cpp:10:7>, void>' requested here
        doit([p = std::move(p)] () { std::cout << *p << std::endl; });
             ^
test.cpp:10:8: note: copy constructor of '' is implicitly deleted because field '' has a deleted
      copy constructor
        doit([p = std::move(p)] () { std::cout << *p << std::endl; });
              ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/bits/unique_ptr.h:273:7: note: 
      'unique_ptr' has been explicitly marked deleted here
      unique_ptr(const unique_ptr&) = delete;
      ^

Есть ли разумный обходной путь?

Тестирование с помощью Ubuntu clang версии 3.5-1~exp1 (магистраль)


person Joseph Mansfield    schedule 30.12.2013    source источник
comment
Это проблема, с которой я сталкивался все чаще и чаще с прокси-серверами (последний раз, когда она всплыла для меня, я реализовал PIMPL в общем виде). Проблема в том, что способности объекта определяются его типом (здесь std::function<void()>), поэтому, если вам нужен объект, который можно копировать только в том случае, если переданный объект копируется, перемещаемый, если переданный объект перемещается и т. д., к сожалению, это невозможно с тот же тип => Я думаю, битовая маска в качестве другого параметра шаблона (std::function<void(), MoveConstructible>) подойдет, но это может быть скучно :/   -  person Matthieu M.    schedule 30.12.2013
comment
Для этой цели у меня есть собственный держатель для стирания шрифта. Насколько мне известно, нет никаких предложений по улучшению ситуации, хотя мне сказали, что есть комментарии от национальных органов по этой теме.   -  person Luc Danton    schedule 31.12.2013
comment
Вероятно, std::function_ref сможет справиться с этим открытым std.org/jtc1/sc22/wg21/docs/papers/2018/p0792r2.html . В противном случае используйте std::cref, как один из ответов ниже.   -  person alfC    schedule 24.10.2020


Ответы (3)


Есть такой подход:

template< typename signature >
struct make_copyable_function_helper;
template< typename R, typename... Args >
struct make_copyable_function_helper<R(Args...)> {
  template<typename input>
  std::function<R(Args...)> operator()( input&& i ) const {
    auto ptr = std::make_shared< typename std::decay<input>::type >( std::forward<input>(i) );
    return [ptr]( Args... args )->R {
      return (*ptr)(std::forward<Args>(args)...);
    };
  }
};

template< typename signature, typename input >
std::function<signature> make_copyable_function( input && i ) {
  return make_copyable_function_helper<signature>()( std::forward<input>(i) );
}

где мы создаем общий указатель на наши данные, затем создаем копируемую лямбду, которая захватывает этот общий указатель, затем мы оборачиваем эту копируемую лямбду в std::function запрошенной подписи.

В вашем случае выше вы бы просто:

doit( make_copyable_function<void()>( [p = std::move(p)] () { std::cout << *p << std::endl; } ) );

Немного более продвинутая версия откладывает стирание типа и добавляет уровень идеальной пересылки для уменьшения накладных расходов:

template<typename input>
struct copyable_function {
  typedef typename std::decay<input>::type stored_input;
  template<typename... Args>
  auto operator()( Args&&... args )->
    decltype( std::declval<input&>()(std::forward<Args>(args)...) )
  {
    return (*ptr)(std::forward<Args>(args));
  }
  copyable_function( input&& i ):ptr( std::make_shared<stored_input>( std::forward<input>(i) ) ) {}
  copyable_function( copyable_function const& ) = default;
private:
  std::shared_ptr<stored_input> ptr;
};
template<typename input>
copyable_function<input> make_copyable_function( input&& i ) {
  return {std::forward<input>(i)}; 
}

который не требует от вас передачи подписи и может быть немного более эффективным в некоторых случаях, но использует более неясные методы.

В C++14 это можно сделать еще короче:

template< class F >
auto make_copyable_function( F&& f ) {
  using dF=std::decay_t<F>;
  auto spf = std::make_shared<dF>( std::forward<F>(f) );
  return [spf](auto&&... args)->decltype(auto) {
    return (*spf)( decltype(args)(args)... );
  };
}

полностью отказаться от вспомогательного типа.

person Yakk - Adam Nevraumont    schedule 30.12.2013

Если время жизни объекта закрытия не является проблемой, вы можете передать его в ссылочной оболочке:

int main()
{
    std::unique_ptr<int> p(new int(5));
    auto f = [p = std::move(p)]{
        std::cout << *p << std::endl;
    };
    doit(std::cref(f));
}

Очевидно, что это не применимо к каждому сценарию, но подойдет для вашей программы-примера.

РЕДАКТИРОВАТЬ: взглянув на N3797 (рабочий проект С++ 14) § 20.9.11.2.1 [func.wrap.func.con] p7, требование CopyConstructible все еще существует. Интересно, есть ли техническая причина, по которой нельзя перейти к MoveConstructible, или комитет просто не удосужился этим заняться?

РЕДАКТИРОВАТЬ: Отвечая на мой собственный вопрос: std::function - это CopyConstructible, поэтому обернутый функтор также должен быть CopyConstructible.

person Casey    schedule 30.12.2013
comment
Если закрытие объекта не является проблемой, не могли бы вы просто сделать так, чтобы он принимал константную функцию‹void()›&? - person IdeaHat; 30.12.2013
comment
@MadScienceDreams Это может работать с конкретной реализацией, создающей объект функции как std::function<void()> bar{std::move(f)};, если эта реализация избегает создания экземпляра конструктора копирования std::function. Однако это не было бы переносимым поведением, поскольку std::function явно требует, чтобы функтор был копируемым в [func.wrap.func.con]/7. Действительно, например, с gcc 4.8.1 это не работает. - person Casey; 30.12.2013
comment
@MadScienceDreams Я предполагаю, что реализации std::function достигают стирания типа, обертывая переданный функтор абстрактным интерфейсом, который включает в себя конструктор виртуальной копии. Каждая реализация C++, с которой я знаком, создает экземпляры всех элементов шаблона, объявленных виртуальными, для заполнения виртуальной таблицы, даже несмотря на то, что стандарт говорит, что создание экземпляров неиспользуемых виртуальных членов зависит от реализации. Очевидно, что конструктор виртуальной копии является источником требования CopyConstructible для функторов, переданных в std::function. - person Casey; 30.12.2013
comment
В черновиках C++0x не всегда было требование CopyConstructible, оно было добавлено LWG 1287. Реализация не должна использовать какие-либо виртуальные функции (и реализация GCC этого не делает), но стирание типа по-прежнему означает, что вы не можете предоставить копируемую оболочку, если цель не может быть копируемой. - person Jonathan Wakely; 17.01.2014

Если вы знаете, что на самом деле не собираетесь копировать свой объект функции, вы можете просто обернуть его в тип, который заставит компилятор думать, что его можно скопировать:

struct ThrowOnCopy {
  ThrowOnCopy() = default;
  ThrowOnCopy(const ThrowOnCopy&) { throw std::logic_error("Oops!"); }
  ThrowOnCopy(ThrowOnCopy&&) = default;
  ThrowOnCopy& operator=(ThrowOnCopy&&) = default;
};

template<typename T>
  struct FakeCopyable : ThrowOnCopy
  {
    FakeCopyable(T&& t) : target(std::forward<T>(t)) { }

    FakeCopyable(FakeCopyable&&) = default;

    FakeCopyable(const FakeCopyable& other)
    : ThrowOnCopy(other),                             // this will throw
      target(std::move(const_cast<T&>(other.target))) // never reached
    { }

    template<typename... Args>
      auto operator()(Args&&... a)
      { return target(std::forward<Args>(a)...); }

    T target;
  };


template<typename T>
  FakeCopyable<T>
  fake_copyable(T&& t)
  { return { std::forward<T>(t) }; }


// ...

doit( fake_copyable([p = std::move(p)] () { std::cout << *p << std::endl; }) );

Шаблон функции fake_copyable создает оболочку, которая является CopyConstructible согласно компилятору (и <type_traits>), но не может быть скопирована во время выполнения.

Если вы сохраните FakeCopyable<X> в std::function, а затем в конечном итоге скопируете std::function, вы получите std::logic_error, но если вы переместите только std::function, все будет работать нормально.

target(std::move(const_cast<T&>(other.target))) выглядит тревожно, но этот инициализатор никогда не запустится, потому что инициализатор базового класса сработает первым. Таким образом, тревожное const_cast на самом деле никогда не происходит, оно просто делает компилятор счастливым.

person Jonathan Wakely    schedule 17.01.2014
comment
Это решение требует, чтобы T был конструируемым по умолчанию. - person Sogartar; 10.01.2019
comment
@Sogartar хороший улов, спасибо. Я исправил код, теперь он работает. - person Jonathan Wakely; 10.01.2019