Привязать структуру только для перемещения к функции

Мне нужно привязать структуру с удаленным конструктором копирования к функции. Я сократил то, чего пытаюсь достичь, до следующего минимального примера:

struct Bar {
    int i;
    Bar() = default;
    Bar(Bar&&) = default;
    Bar(const Bar&) = delete;
    Bar& operator=(const Bar&) = delete;
};

void foo(Bar b) {
    std::cout << b.i << std::endl;
}

int main()
{
    Bar b;
    b.i = 10;

    std::function<void()> a = std::bind(foo, std::move(b)); // ERROR
    a();

    return 0;
}

От компилятора получаю только плач и скрежет зубов:

test.cpp:22:27: error: no viable conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar), Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'std::function<void ()>'
    std::function<void()> a = std::bind(foo, std::move(b));
                          ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2013:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'nullptr_t' for 1st argument
      function(nullptr_t) noexcept
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2024:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'const std::function<void ()> &' for 1st argument
      function(const function& __x);
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2033:7: note: candidate constructor not viable: no known conversion from 'typename _Bind_helper<__is_socketlike<void (&)(Bar)>::value, void (&)(Bar),
      Bar>::type' (aka '_Bind<__func_type (typename decay<Bar>::type)>') to 'std::function<void ()> &&' for 1st argument
      function(function&& __x) : _Function_base()
      ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.1.0/../../../../include/c++/5.1.0/functional:2058:2: note: candidate template ignored: substitution failure [with _Functor = std::_Bind<void (*(Bar))(Bar)>]: no matching function for call to object of
      type 'std::_Bind<void (*(Bar))(Bar)>'
        function(_Functor);
        ^
1 error generated.

Поэтому я хотел бы спросить, есть ли какой-нибудь обходной путь, который позволил бы мне связать Bar с foo, оставив Bar только для перемещения.

Изменить. Также рассмотрите следующий код, в котором жизнь переменной b заканчивается до вызова a:

int main()
{
    std::function<void()> a;
    {
        Bar b;
        b.i = 10;
        a = std::bind(foo, std::move(b)); // ERROR
    }
    a();

    return 0;
}

person Jendas    schedule 25.08.2015    source источник


Ответы (2)


std::function не может принимать вызовы только для перемещения. Он стирает переданный тип для вызова (с подписью), уничтожения и копирования.1

Написание std::function только для перемещения — это совсем немного работы. Вот пример в другом контексте. живой пример.

Забавно, что std::packaged_task также является вызывающим средством стирания типов только для перемещения, но он тяжелее, чем вам, вероятно, хотелось бы, и получение значения - это боль.

Более простым решением является злоупотребление общим указателем:

template<class F>
auto shared_function( F&& f ) {
  auto pf = std::make_shared<std::decay_t<F>>(std::forward<F>(f));
  return [pf](auto&&... args){
    return (*pf)(decltype(args)(args)...);
  };
}

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

Это иллюстрирует проблему - вызов не работает! Все вышеперечисленное имеет вызов const.

Вам нужна задача, которую можно вызвать только один раз.

template<class Sig>
struct task_once;

namespace details_task_once {
  template<class Sig>
  struct ipimpl;
  template<class R, class...Args>
  struct ipimpl<R(Args...)> {
    virtual ~ipimpl() {}
    virtual R invoke(Args&&...args) && = 0;
  };
  template<class Sig, class F>
  struct pimpl;
  template<class R, class...Args, class F>
  struct pimpl<R(Args...), F>:ipimpl<R(Args...)> {
    F f;
    template<class Fin>
    pimpl(Fin&&fin):f(std::forward<Fin>(fin)){}
    R invoke(Args&&...args) && final override {
      return std::forward<F>(f)(std::forward<Args>(args)...);
    };
  };
  // void case, we don't care about what f returns:
  template<class...Args, class F>
  struct pimpl<void(Args...), F>:ipimpl<void(Args...)> {
    F f;
    template<class Fin>
    pimpl(Fin&&fin):f(std::forward<Fin>(fin)){}
    void invoke(Args&&...args) && final override {
      std::forward<F>(f)(std::forward<Args>(args)...);
    };
  };
}
template<class R, class...Args>
struct task_once<R(Args...)> {
  task_once(task_once&&)=default;
  task_once&operator=(task_once&&)=default;
  task_once()=default;
  explicit operator bool() const { return static_cast<bool>(pimpl); }

  R operator()(Args...args) && {
    auto tmp = std::move(pimpl);
    return std::move(*tmp).invoke(std::forward<Args>(args)...);
  }
  // if we can be called with the signature, use this:
  template<class F,
    class R2=R,
    std::enable_if_t<
        std::is_convertible<std::result_of_t<F&&(Args...)>,R2>{}
        && !std::is_same<R2, void>{}
    >* = nullptr
  >
  task_once(F&& f):task_once(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}

  // the case where we are a void return type, we don't
  // care what the return type of F is, just that we can call it:
  template<class F,
    class R2=R,
    class=std::result_of_t<F&&(Args...)>,
    std::enable_if_t<std::is_same<R2, void>{}>* = nullptr
  >
  task_once(F&& f):task_once(std::forward<F>(f), std::is_convertible<F&,bool>{}) {}

  // this helps with overload resolution in some cases:
  task_once( R(*pf)(Args...) ):task_once(pf, std::true_type{}) {}
  // = nullptr support:
  task_once( std::nullptr_t ):task_once() {}

private:
  std::unique_ptr< details_task_once::ipimpl<R(Args...)> > pimpl;

// build a pimpl from F.  All ctors get here, or to task() eventually:
  template<class F>
  task_once( F&& f, std::false_type /* needs a test?  No! */ ):
    pimpl( new details_task_once::pimpl<R(Args...), std::decay_t<F>>{ std::forward<F>(f) } )
  {}
  // cast incoming to bool, if it works, construct, otherwise
  // we should be empty:
  // move-constructs, because we need to run-time dispatch between two ctors.
  // if we pass the test, dispatch to task(?, false_type) (no test needed)
  // if we fail the test, dispatch to task() (empty task).
  template<class F>
  task_once( F&& f, std::true_type /* needs a test?  Yes! */ ):
    task_once( f?task_once( std::forward<F>(f), std::false_type{} ):task_once() )
  {}
};

живой пример.

Обратите внимание, что вы можете вызывать () только в контексте rvalue с указанным выше task_once. Это потому, что () деструктивен, как и должно быть в вашем случае.

К сожалению, приведенное выше основано на C++14. И сейчас мне не нравится писать код на C++11. Итак, вот более простое решение C++11, которое менее производительно:

std::function<void()> a;
{
    Bar b;
    b.i = 10;
    auto pb = std::make_shared<Bar>(std::move(b));
    a = [pb]{ return foo(std::move(*pb)); };
}
a();

Это помещает перемещенную копию b в общий указатель, сохраняет ее в std::function, а затем уничтожает ее при первом вызове ().


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

person Yakk - Adam Nevraumont    schedule 25.08.2015
comment
Вау, хорошо, это выглядит сложно. Но будет ли ваше решение работать с кодом, который я разместил в редактировании? - person Jendas; 25.08.2015
comment
@Jendas извините, у task_once возникла проблема с типом возврата void. Исправлено: см. новый живой пример. Просто замените std::function на task_once и используйте изменяемые лямбда-выражения с захватом движений вместо std::bind. std::bind редко бывает хорошей идеей. - person Yakk - Adam Nevraumont; 25.08.2015
comment
@Yakk: Я придумал решение, похожее на ваше C++ 11, в вашем редактировании. Правильно ли я говорю, что вы можете вызвать std::function только один раз, после чего он использует ваш объект, и вы не должны вызывать его во второй раз? - person AndyG; 25.08.2015
comment
@AndyG, если вы вызовете его во второй раз (или его копию во второй раз), вы в конечном итоге вызовете его для перемещенного объекта. Это может быть или не быть тем, чего вы хотите. В любом случае функциональный объект OP может быть вызван только один раз (до вызова перемещенного объекта), поскольку у нас есть захваченное значение только для перемещения, вызывающее функцию, которая принимает свой аргумент по значению. - person Yakk - Adam Nevraumont; 25.08.2015

Вы можете обойти ограничение CopyConstructible std::function с помощью комбинации указателей, лямбда-выражений и std::bind:

auto lambda = [](Bar* b){::foo(std::move(*b));};
std::function<void()> a = std::bind(lambda, &b);
a();

Пример


Редактировать

Однострочный код на C++11 с лямбдой и захватом по ссылке

std::function<void()> a = [&b](){::foo(std::move(b));};
a()

Пример 2

Редактировать2

(Перемещение комментария в мой ответ)

После редактирования вашего кода, которое добавляет ограничение, согласно которому объект функции должен иметь возможность пережить область действия переменной, привязанной к функции, мы можем выполнить это по-прежнему с помощью лямбда, только теперь мы должны захватить shared_ptr, который использует выделение и перемещение конструкции держать Bar.

В приведенном ниже примере я использую обобщенный захват C++14 для захвата файла shared_ptr. решение @Yakk переводит это на C++11.

std::function<void()> a;
{
    Bar b;
    b.i = 10;
    a = [b2 = std::make_shared<decltype(b)>(std::move(b))]()
    {
        // move the underlying object out from under b2
        // which means b2 is in a valid but undefined state afterwards
        ::foo(std::move(*b2));
    }; 
}

Пример 3

person AndyG    schedule 25.08.2015
comment
Я ценю ваш быстрый ответ, но я сомневаюсь, что этот подход эквивалентен использованию привязки. Поправьте меня, если я ошибаюсь, но если что-то вызовет деструктор перед a(), код приведет к неопределенному поведению или что-то в этом роде. Это точно не то, что мне нужно. - person Jendas; 25.08.2015
comment
Я сократил ваш код до coliru.stacked-crooked.com/a/62d45f89ae2911bc очень похоже на то, что мне нужно. Но как это работает? - person Jendas; 25.08.2015
comment
@Jendas: Извините, пожалуйста, не делайте этого. Построение перемещения не происходит до тех пор, пока не будет вызвана функция, и в этот момент ваша переменная уже была уничтожена, поэтому это может привести к неопределенному поведению. - person AndyG; 25.08.2015
comment
Да, я так и думал :-/ Выглядит нормально, но находится в области неопределенного поведения. Этот сакс. - person Jendas; 25.08.2015
comment
@Jendas: я думаю, это сделает то, что вы хотите, но вы можете вызвать свою функцию только один раз! coliru.stacked-crooked.com/a/c98b9e2b1071a916 - person AndyG; 25.08.2015
comment
@Jendas: я должен отметить, что код в этой ссылке — C++14. Эквивалент С++ 11 см. в редактировании Yakk. - person AndyG; 25.08.2015