Зачем использовать идеально переданное значение (функтор)?

В C++11 (и C++14) представлены дополнительные языковые конструкции и усовершенствования, предназначенные для универсального программирования. К ним относятся такие функции, как;

  • Ссылки R-значения
  • Ссылка сворачивается
  • Идеальная переадресация
  • Переместить семантику, вариативные шаблоны и многое другое

Я просматривал более ранний черновик спецификация C++14 (теперь с обновленным текстом) и код в примере в §20.5.1, Целочисленные последовательности времени компиляции, которые мне показались интересными и необычными.

template<class F, class Tuple, std::size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
  return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...);
}

template<class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
  using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;
  return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices());
}

Онлайн здесь [intseq.general]/2.

Вопрос

  • Почему была переадресована функция f в apply_impl, т.е. почему std::forward<F>(f)(std::get...?
  • Почему бы просто не применить функцию как f(std::get...?

person Niall    schedule 16.07.2014    source источник


Ответы (2)


Вкратце...

В TL;DR вы хотите сохранить категорию значений (природа r-value/l-value) функтора, потому что это может повлиять на разрешение перегрузки, в частности квалифицированный по ссылке участники.

Сокращение определения функции

Чтобы сосредоточиться на проблеме переадресации функции, я сократил образец (и компилировал его с помощью компилятора C++11) до;

template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
  return std::forward<F>(func)(std::forward<Args>(args)...);
}

И мы создаем вторую форму, где заменяем std::forward(func) только на func;

template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
  return func(std::forward<Args>(args)...);
}

Образец оценки

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

struct Functor1 {
  int operator()(int id) const
  {
    std::cout << "Functor1 ... " << id << std::endl;
    return id;
  }
};

Исходный образец

Запустите пример кода;

int main()
{
  Functor1 func1;
  apply_impl_2(func1, 1);
  apply_impl_2(Functor1(), 2);
  apply_impl(func1, 3);
  apply_impl(Functor1(), 4);
}

И результат соответствует ожидаемому, независимо от того, используется ли r-значение Functor1() или l-значение func при вызове apply_impl и apply_impl_2 вызывается оператор перегруженного вызова. Он вызывается как для r-значений, так и для l-значений. В C++03 это было все, что у вас было: вы не могли перегружать методы-члены на основе r-value-ness или l-value-ness объекта.

Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4

Образцы, соответствующие требованиям

Теперь нам нужно перегрузить этот оператор вызова, чтобы немного растянуть его...

struct Functor2 {
  int operator()(int id) const &
  {
    std::cout << "Functor2 &... " << id << std::endl;
    return id;
  }
  int operator()(int id) &&
  {
    std::cout << "Functor2 &&... " << id << std::endl;
    return id;
  }
};

Мы запускаем еще один набор образцов;

int main()
{
  Functor2 func2;
  apply_impl_2(func2, 5);
  apply_impl_2(Functor2(), 6);
  apply_impl(func2, 7);
  apply_impl(Functor2(), 8);
}

И выход;

Функтор2 &... 5
Функтор2 &... 6
Функтор2 &... 7
Функтор2 &&... 8

Обсуждение

В случае apply_impl_2 (id 5 и 6) вывод не такой, как можно было изначально ожидать. В обоих случаях вызывается l-значение с уточнением operator() (r-значение вообще не вызывается). Можно было ожидать, что, поскольку Functor2(), r-значение, используется для вызова apply_impl_2, будет вызвано r-значение с уточнением operator(). func, как именованный параметр для apply_impl_2, является ссылкой на r-значение, но, поскольку он именован, он сам является l-значением. Следовательно, квалифицированное l-значение operator()(int) const& вызывается как в случае, когда l-значение func2 является аргументом, так и в случае использования r-значения Functor2() в качестве аргумента.

В случае apply_impl (id 7 и 8) std::forward<F>(func) поддерживает или сохраняет природу r-значения/l-значения аргумента, предусмотренного для func. Следовательно, l-значение с указанием operator()(int) const& вызывается с l-значением func2, используемым в качестве аргумента, и r-значением с уточнением operator()(int)&&, когда в качестве аргумента используется r-значение Functor2(). Такое поведение было ожидаемым.

Выводы

Использование std::forward посредством идеальной переадресации гарантирует, что мы сохраним характер r-value/l-value исходного аргумента для func. Он сохраняет их категорию значений.

Это необходимо, std::forward можно и нужно использовать не только для переадресации аргументов функциям, но также и тогда, когда требуется использование аргумента, где природа r-value/l-value должна быть сохраненным. Примечание; бывают ситуации, когда r-значение/l-значение не может или не должно сохраняться, в этих ситуациях std::forward не следует использовать (см. обратное ниже).

Появляется много примеров, в которых непреднамеренно теряется характер аргументов r-значение/l-значение из-за, казалось бы, невинного использования ссылки на r-значение.

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

Полный пример кода можно найти здесь

Следствие и обратное

  • Следствием вопроса будет; если ссылка рушится в шаблонной функции, как поддерживается характер аргумента r-value/l-value? Ответ - используйте std::forward<T>(t).
  • Конверс; решает ли std::forward все ваши проблемы с универсальными ссылками? Нет, бывают случаи, когда не следует использовать, например, пересылка значения более одного раза.

Краткая предыстория идеальной переадресации

Идеальная переадресация может быть незнакома некоторым, так что же такое идеальная переадресация?

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

Скотт Мейерс приводит следующий псевдокод в своем Презентация Going Native 2013 для объяснения работы std::forward (примерно на 20-й минуте);

template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
  if (is_lvalue_reference<T>::value) {
    return param; // return type T&& collapses to T& in this case
  }
  else {
    return move(param);
  }
}

Идеальная переадресация зависит от нескольких фундаментальных языковых конструкций, новых для C++11, которые формируют основу для большей части того, что мы сейчас видим в обобщенном программировании:

  • Ссылка сворачивается
  • Ссылки Rvalue
  • Семантика перемещения

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

Томас Беккер (Thomas Becker) дает хороший, но емкий отчет о проблеме и решение.

Что такое реф-квалификаторы?

ref-qualifiers (lvalue ref-qualifier & и rvalue ref-qualifier &&) аналогичны cv-qualifiers тем, что они (члены с квалификацией ref) используются во время разрешение перегрузки, чтобы определить, какой метод вызывать. Они ведут себя так, как вы от них ожидаете; & применяется к lvalue и && к rvalue. Примечание. В отличие от квалификации cv, *this остается выражением l-значения.

person Niall    schedule 16.07.2014
comment
Сегодня я открываю реф-квалифицированные методы. Мой разум был взорван. - person Quentin; 16.07.2014
comment
Дополнительная ссылка на Преимущества пересылки в SO - person Niall; 25.07.2014

Вот практический пример.

struct concat {
  std::vector<int> state;
  std::vector<int> const& operator()(int x)&{
    state.push_back(x);
    return state;
  }
  std::vector<int> operator()(int x)&&{
    state.push_back(x);
    return std::move(state);
  }
  std::vector<int> const& operator()()&{ return state; }
  std::vector<int> operator()()&&{ return std::move(state); }
};

Этот функциональный объект принимает x и объединяет его с внутренним std::vector. Затем он возвращает этот std::vector.

При оценке в контексте rvalue он moves является временным, в противном случае он возвращает const& внутреннему вектору.

Теперь вызываем apply:

auto result = apply( concat{}, std::make_tuple(2) );

поскольку мы тщательно пересылали наш объект функции, выделяется только 1 буфер std::vector. Он просто перемещен в result.

Без тщательной переадресации мы в конечном итоге создадим внутренний std::vector и скопируем его в result, а затем отбросим внутренний std::vector.

Поскольку operator()&& знает, что объект функции следует рассматривать как значение r, которое вот-вот будет уничтожено, он может вырвать кишки из объекта функции, выполняя свою операцию. operator()& не может этого сделать.

Тщательное использование идеальной переадресации функциональных объектов обеспечивает эту оптимизацию.

Обратите внимание, однако, что на данный момент эта техника очень мало используется «в дикой природе». Перегрузка с уточнением Rvalue неясна, и делает это operator() тем более.

Тем не менее, я мог бы легко увидеть будущие версии C++, автоматически использующие состояние rvalue лямбды для неявного move ее данных, захваченных по значению, в определенных контекстах.

person Yakk - Adam Nevraumont    schedule 16.07.2014