Подтвердить что ты не робот

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

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

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

Я просматривал ранее проект спецификация С++ 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());
}

Online здесь [intseq.general]/2.

Вопрос

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

Ответ 1

Вкратце...

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

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

Чтобы сосредоточиться на проблеме пересылаемой функции, я уменьшил образец (и скомпилировал его с компилятором С++ 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-value func при вызове apply_impl и apply_impl_2 вызывается оператор перегруженного вызова. Он вызывается как для r-значений, так и для l-значений. В С++ 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);
}

И результат:

Functor2 &... 5
Functor2 &... 6
Functor2 &... 7
Functor2 &... 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-value func2, так и в качестве аргумента используется r-значение Functor2().

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

Выводы

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

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

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

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

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

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

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

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

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

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

Скотт Мейерс дает следующий псевдокод в своем Going Native 2013 presentation, чтобы объяснить работу 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);
  }
}

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

  • Сбой ссылок
  • Ссылки на Rvalue
  • Переместить семантику

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

Томас Беккер предлагает приятную, но плотную запись о совершенной пересылке проблема и .

Что такое ref-qualifiers?

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

Ответ 2

Вот пример.

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, он move временный, в противном случае он возвращает const& во внутренний вектор.

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

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

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

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

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

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

Обратите внимание, однако, что в этот момент очень мало используется этот метод "в дикой природе". Rvalue квалифицированная перегрузка неясна и делает это operator() moreso.

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