В чем разница между передачей функции напрямую в std :: async и использованием std :: bind? - PullRequest
4 голосов
/ 15 июня 2019

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

return executeRequest<int>(false, d, &callback, false);

Это было до добавления поддержки асинхронности. Я попытался изменить его на:

return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);

Но это не удалось скомпилировать.

MCVE:

#include <iostream>
#include <future>

int callback(const int& t) {
    std::cout << t << std::endl;   
    return t;
}
class RequestData {
private:
    int x;
public:
    int& getX() {
        return x;   
    }
};
class X {
    public:
        template <typename T>
        T executeRequest(bool method, RequestData& requestData,
                       std::function<T(const int&)> parser, bool write) {
            int ref = 42;
            std::cout << requestData.getX() << std::endl;
            return parser(ref);
        }
        int nonAsync() {
            // Compiles 
            RequestData d;
            return this->executeRequest<int>(false, d, &callback, false);    
        }
        std::future<int> getComments() {
            RequestData d;
            // Doesn't compile 
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
        }
};

int main() {
    X x;
    auto fut = x.getComments();
    std::cout << "end: " << fut.get() << std::endl;
}

И это не с:

In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1505:56: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
      typedef typename result_of<_Callable(_Args...)>::type result_type;
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:1709:49: note: in instantiation of template class 'std::_Bind_simple<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>' requested here
          __state = __future_base::_S_make_async_state(std::__bind_simple(
                                                       ^
main.cpp:33:25: note: in instantiation of function template specialization 'std::async<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool), X *, bool, RequestData &, int (*)(const int &), bool>' requested here
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
                        ^
In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1525:50: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
        typename result_of<_Callable(_Args...)>::type
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
2 errors generated.

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

Единственное действительное различие между этими двумя (по крайней мере, что я вижу визуально) заключается в том, что мне нужно явно передать this, потому что я ссылаюсь на функцию-член

Я немного поиграл с этим, и мне удалось обнаружить, что если я заменю его на const RequestData&, он внезапно разрешится. Но вместо этого это приводит к проблемам в другом месте, потому что получатель не const. По крайней мере, из того, что я смог найти, мне нужно сделать ее константной функцией, что хорошо для самого геттера, но у меня также есть некоторые сеттеры, означающие, что я не могу с этим согласиться.

В любом случае, я решил попробовать std::bind. Я заменил асинхронный вызов на:

auto func = std::bind(&X::executeRequest<int>, this, false, d, &callback, false);
return std::async(std::launch::async, func);

И по какой-то причине сработало .

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

Я покопался глубже и нашел несколько альтернативных решений (хотя ссылаясь на std::thread), которые использовали std::ref. Я знаю, что std::async работает std::thread под капотом, поэтому я откопал документацию :

Аргументы функции потока перемещаются или копируются по значению. Если ссылочный аргумент необходимо передать функции потока, он должен быть заключен в оболочку (например, с помощью std::ref или std::cref). (выделение мое)

Это имеет смысл и объясняет, почему это не удалось. Я предполагаю, что std::async также ограничен этим и объясняет, почему это не удалось.

Однако, выкапывая std :: bind :

Аргументы для привязки копируются или перемещаются, и никогда не передаются по ссылке , если они не заключены в std::ref или std::cref. (выделение мое)

Я не использую std::ref (или если я заменю на const, std::cref) в любом из них, но, по крайней мере, если я правильно понял документацию, оба из них не должны компилироваться. Пример на cppreference.com также компилируется без std::cref (протестировано в Coliru с Clang и C ++ 17).

Что здесь происходит?

Если это имеет значение, за исключением среды coliru, я изначально воспроизводил проблему в Docker с Ubuntu 18.04 с Clang 8.0.1 (64-битная версия). Скомпилировано против C ++ 17 в обоих случаях.

Ответы [ 2 ]

5 голосов
/ 15 июня 2019

В стандарте есть небольшие отличия. Для std::bind:

Требуется: is_­constructible_­v<FD, F> должно быть true. Для каждого Ti в BoundArgs, is_­constructible_­v<TDi, Ti> должно составлять true. INVOKE(fd, w1, w2, …, wN) ([func.require]) должно быть допустимым выражением для некоторых значений w1, w2,…, wN, где N имеет значение sizeof...(bound_­args). Как указано ниже, cv-квалификаторы cv оболочки вызовов g не должны быть ни volatile, ни const volatile.

Возвращает: оболочка переадресации аргументов g ([func.require]). Эффект g(u1, u2, …, uM) должен быть

INVOKE(fd, std::forward<V1>(v1), std::forward<V2>(v2), …, std::forward<VN>(vN))

Где v1, ..., vN имеют определенные типы. В вашем случае важно, чтобы хранимая переменная, соответствующая d, имела тип std::decay_t<RequestData&>, который равен RequestData. В этом случае вы можете легко позвонить executeRequest<int> с lvalue RequestData.

Требования для std::async намного сильнее:

Требуется: F и каждый Ti в Args должен удовлетворять требованиям Cpp17MoveConstructible и

INVOKE(decay-copy(std::forward<F>(f)),
   decay-copy(std::forward<Args>(args))...)     // see [func.require], [thread.thread.constr]

Огромная разница составляет распад-копия . Для d вы получаете следующее:

decay-copy(std::forward<RequestData&>(d))

Это вызов функции decay-copy (только экспозиция), тип возвращаемого значения которой std::decay_t<RequestData&>, поэтому RequestData, поэтому компиляция не удалась.


Обратите внимание, что если вы используете std::ref, поведение будет неопределенным, поскольку время жизни d может закончиться до вызова executeRequest.

2 голосов
/ 15 июня 2019

Меня смущает то, что оба раза используются одни и те же аргументы

Но он не пересылает их одинаково оба раза. При вызове асинхронной версии вызываемый объект вызывается , как если бы он вызывал :

std::invoke(decay_copy(std::forward<Function>(f)), 
            decay_copy(std::forward<Args>(args))...);

Аргументы превращаются в нечто сродни временникам! По этой причине ссылка RequestData& requestData не может связываться со своим аргументом. Здесь может работать константная ссылка, ссылка на rvalue или простой аргумент по значению (как в bind), но неконстантная ссылка на lvalue не может.

std::bind делает свой вызов по-другому. Он также хранит копии, но "обычный хранимый аргумент arg передается вызываемому объекту как аргумент lvalue [sic]" с квалификацией cv аргумента, полученного из самого объекта bind. Поскольку std::bind создает объект неконстантного связывания, вызываемому предоставляется неконстантное lvalue для requestData. Ссылка на это счастливо.

...