что происходит в std :: function operator () и std :: forward?

Я смотрел реализацию std::function и ее вызов operator()

template<typename Ret, typename... ArgTypes>
Ret function< Ret (ArgTypes...)>::operator()(ArgTypes...args) const
{
  // some stuff
  return invoker(functor, std::forward<ArgTypes>(args)...);
}

Мне было особенно интересно, почему здесь используется std::forward? Это как-то связано с безупречной переадресацией? Поскольку безупречная пересылка может быть выполнена только в том случае, если operator() является шаблоном с вариативным объявлением шаблона template<typename... Args> (а это не так, объявление является частичной специализацией std :: function). Каково намерение здесь использовать std :: forward? Я смущен :-)?


person Gabriel    schedule 05.03.2017    source источник
comment
«чего нет» Не так ли? Конечно, мне кажется.   -  person Biffen    schedule 05.03.2017
comment
объявление является полной специализацией std :: function и templ. параметры Ret, ArgTypes уже исправлены   -  person Gabriel    schedule 05.03.2017
comment
Мне кажется, что это функция-член частичной специализации шаблона класса. Я не уверен, где вы видите полную специализацию (полная специализация будет иметь пустой список параметров шаблона).   -  person    schedule 05.03.2017
comment
@ Дмитрий Кузнецов Вы правы!   -  person Gabriel    schedule 05.03.2017


Ответы (3)


Вы правы, что это не ваш типичный сценарий "идеальной пересылки". Краткий пример может помочь проиллюстрировать мотивацию. Предположим, что тип A с инструментальными конструкторами и деструктором:

#include "A.h"
#include <functional>
#include <iostream>

int
main()
{
    A a1{1};
    A a2{2};
    std::function<void(A, A&)> f{[](A x, A& y){}};
    f(a1, a2);
}

Это выведет:

A(int state): 1
A(int state): 2
A(A const& a): 1
A(A&& a): 1
~A(1)
~A(-1)
~A(2)
~A(1)

Объяснение:

a1 и a2 построены в стеке. Затем при передаче в function invoker, a1 сначала копируется для привязки к первому параметру по значению, а затем std::forward<A> вызывается в a1, который перемещает его из параметра по значению в лямбда.

Напротив, a2 не нужно копировать для привязки к параметру function A&, а затем вызывается std::forward<A&>(a2), который пересылает a2 как lvalue вместо rvalue, и это связывается с параметром A& лямбда.

Тогда все рушится. ~A(-1) указывает на уничтожение A в состоянии, созданном с помощью этого инструмента, A.

Таким образом, даже несмотря на то, что ArgTypes не выводится, как в обычной идиоме идеальной пересылки, мы по-прежнему хотим пересылать по значению ArgTypes как rvalue, а по ссылке ArgTypes как lvalues. Итак, std::forward просто делает именно то, что мы хотим здесь.

person Howard Hinnant    schedule 05.03.2017
comment
@ Габриэль: Извините, я не понимаю следующий вопрос. Не могли бы вы быть более конкретным? - person Howard Hinnant; 05.03.2017
comment
Можно ли сформулировать это также так (для меня немного более ясно): ... а затем вызывается std::forward<A>(a1), который возвращает A&&a1 (который является ссылкой rvalue - ›перемещает его в лямбду). ... а затем вызывается std::forward<A&>(a2), который возвращает A& && a2, который сворачивается в A& a2 (который является ссылкой lvalue, - ›пересылка как lvalue). - person Gabriel; 05.03.2017
comment
@Gabriel: std::function сформулирован так, как я описал выше. Если это то, что это означает в вашем вопросе, то это уже так в стандарте. Извините, если я веду себя глупо. Я не спал уже слишком много часов. - person Howard Hinnant; 05.03.2017

Думаю, вас здесь многое смущает.

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

template<typename T>
struct Wrapper {
    template<typename Arg>
    decltype(auto) test(Arg&& arg) {
        return t.test(std::forward<Arg>(arg));
    }

    T t;
};

Обратите внимание на то, что здесь используется идеальная переадресация без вариативных шаблонов. Если t.test потребует в качестве параметра тип только для перемещения, его невозможно будет вызвать без forward<Arg>(arg).


Второе, что здесь происходит, - это параметр, за которым не следует &&. Добавление && к ArgTypes было бы ошибкой и могло бы привести к сбою компиляции в некоторых случаях. Рассмотрим этот простой случай:

std::function<void(int)> f;
int i = 0;
f(i);

Это не скомпилируется. Если вы добавите && к ArgTypes, все параметры, которые не являются ссылочными (например, int), станут ссылкой на rvalue в операторе вызова (в нашем случае int&&). Поскольку все типы параметров уже правильно определены в списке аргументов std::function, вы хотите получить в операторе вызова именно эти типы, а не преобразованные.

Почему вам нужен std::forward, если вы не используете &&? Потому что, даже если вам не нужно выводить категории значений, вам все равно не нужно копировать все аргументы в содержащуюся функцию. Если один из параметров std::function равен int&, вы не хотите его перемещать. Но если один из параметров std::unique_ptr<int>, вы должны его переместить! И это именно то, для чего нужен std::forward. Перемещается только то, что следует переместить.

person Guillaume Racicot    schedule 05.03.2017

std::forward просто добавляет ссылку rvalue к типу, поэтому, принимая во внимание правила сворачивания ссылок, он эффективно передает аргументы ссылки как есть и перемещает аргументы объекта.

person yuri kilochek    schedule 05.03.2017