Как правильно использовать вызываемый, переданный через ссылку переадресации? - PullRequest
2 голосов
/ 22 апреля 2019

Я привык передавать лямбда-функции (и другие функции) к шаблонным функциям - и использовать их - следующим образом

template <typename F>
auto foo (F && f)
 {
   // ...

   auto x = std::forward<F>(f)(/* some arguments */);

   // ... 
 }

Я имею в виду: я привык передавать их через ссылку для пересылки и вызывать их, передавая через std::forward.

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

Я частично неправильно понял, что он имеет в виду (по моей вине), но остаётся сомнение, если следующая функция bar() (с несомненным кратным std::forward над одним и тем же объектом) это правильный код или (возможно, только потенциально) опасно.

template <typename F>
auto bar (F && f)
 {
   using A = typename decltype(std::function{std::forward<F>(f)})::result_type;

   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

Ответы [ 3 ]

4 голосов
/ 22 апреля 2019

Вперед - просто условный ход.

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

Неоцененные форварды ничего не двигают, поэтому они не учитываются.

Маршрутизация через std::function добавляет складку: этот вывод работает только для указателей функций и объектов функций с одним оператором вызова функции, который не квалифицирован &&. Для них вызовы rvalue и lvalue всегда эквивалентны, если оба компилируются.

2 голосов
/ 22 апреля 2019

Я бы сказал, что в этом случае применяется общее правило. Вы не должны ничего делать с переменной после ее перемещения / пересылки, за исключением, возможно, присвоения ей.

Таким образом ...

Как правильно использовать вызываемый, переданный через ссылку на переадресацию?

Только переадресация, если вы уверены, что он больше не будет вызываться (т. Е. При последнем вызове, если вообще будет).

Если он никогда не вызывается более одного раза, нет причин не пересылать.


Что касается того, почему ваш фрагмент кода может быть опасным, рассмотрите следующий функтор:

template <typename T>
struct foo
{
    T value;
    const T &operator()() const & {return value;}
    T &&operator()() && {return std::move(value);}
};

В качестве оптимизации operator() при вызове значения r позволяет вызывающей стороне переходить с value.

Теперь ваш шаблон не скомпилировался бы, если бы был задан этот функтор (потому что, как сказал T.C., std::function не сможет определить тип возвращаемого значения в этом случае).

Но если мы немного его изменим:

template <typename A, typename F>
auto bar (F && f)
 {    
   std::vector<A>  vect;

   for ( auto i { 0u }; i < 10u ; ++i )
      vect.push_back(std::forward<F>(f)());

   return vect;
 }

тогда он эффектно сломается когда ему дадут этот функтор.

1 голос
/ 22 апреля 2019

Если вы собираетесь либо просто переслать вызываемый объект в другое место, либо просто вызвать вызываемый объект ровно один раз, я бы сказал, что использование std::forward - это правильная вещь в целом. Как объяснено здесь , это будет в некотором роде сохранять категорию значений вызываемой и позволять вызывать «правильную» версию потенциально перегруженного оператора вызова функции.

Проблема в исходном потоке заключалась в том, что вызываемый объект вызывался в цикле, поэтому потенциально вызывался более одного раза. Конкретный пример из другой нити был

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(f(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(f(element));

    return sequence;
}

Здесь я считаю, что звонить std::forward<F>(f)(element) вместо f(element), т.е.

template <typename F>
auto map(F&& f) const
{
    using output_element_type = decltype(std::forward<F>(f)(std::declval<T>()));

    auto sequence = std::make_unique<Sequence<output_element_type>>();

    for (const T& element : *this)
        sequence->push(std::forward<F>(f)(element));

    return sequence;
}

будет потенциально проблематично. Насколько я понимаю, определяющей характеристикой r-значения является то, что на него нельзя явно ссылаться. В частности, естественно, нет возможности использовать одно и то же значение в выражении более одного раза (по крайней мере, я не могу придумать одно). Кроме того, насколько я понимаю, если вы используете std::move или std::forward или любой другой способ получения значения xval, даже для того же исходного объекта, результатом будет новое значение xvalue каждый раз. Таким образом, также не может быть способа ссылаться на одно и то же значение x более одного раза. Поскольку одно и то же значение r не может использоваться более одного раза, я бы сказал (см. Также комментарии под этим ответом ), что для перегруженного оператора вызова функции обычно допустимо делать то, что может быть сделано только один раз, если вызов происходит по r-значению, например:

class MyFancyCallable
{
public:
    void operator () & { /* do some stuff */ }
    void operator () && { /* do some stuff in a special way that can only be done once */ }
};

Реализация MyFancyCallable может предполагать, что вызов, который выберет версию, квалифицированную &&, не может произойти более одного раза (для данного объекта). Таким образом, я бы посчитал переадресацию одного и того же вызываемого в более чем один вызов семантически разорванным.

Конечно, технически не существует универсального определения того, что на самом деле означает продвигать или перемещать объект. В конце концов, это действительно зависит от реализации определенных типов, чтобы назначить значение там. Таким образом, вы можете просто указать в качестве части вашего интерфейса, что потенциальные вызовы, передаваемые в ваш алгоритм, должны иметь возможность иметь дело с многократным вызовом по rvalue, который ссылается на один и тот же объект. Однако это в значительной степени идет вразрез со всеми соглашениями о том, как механизм ссылок на rvalue обычно используется в C ++, и я не очень понимаю, что можно было бы получить от этого & hellip;

...