Понимание реализации std::forward начиная с C++11

(constexpr и noexcept опущены, так как они кажутся неуместными для понимания поведения std::forward.)

Основываясь на моем понимании «Эффективного современного C++» Скотта Мейерса, пример реализации std::move в C++14 выглядит следующим образом.

template<typename T>
decltype(auto) move(T&& param) {
  return static_cast<remove_reference_t<T>&&>(param);
}

Учитывая объяснение того, что такое переадресация (или «универсальная») ссылка, эта реализация, я думаю, довольно ясна для меня:

  • параметр param имеет тип T&&, то есть ссылку на rvalue или ссылку на lvalue (в зависимости от типа аргумента), в зависимости от того, является ли аргумент rvalue или lvalue в вызывающей программе; другими словами, param может привязываться как к rvalue, так и к lvalue (т. е. к чему угодно); так и задумано, поскольку move должно привести что угодно к rvalue.
  • decltype(auto) — это просто краткий способ выразить тип возвращаемого значения на основе фактического оператора return.
  • возвращаемый объект является тем же самым объектом param, приведенным к ссылке rvalue (&&) на любой тип T после того, как его выведенная ссылка удалена (вывод выполняется для T&&, а не для ⋯<T>&&).

Короче говоря, мое понимание использования пересылки/универсальных ссылок в реализации move следующее:

  • переадресация/универсальная ссылка T&& используется для параметра, поскольку он предназначен для привязки к чему-либо;
  • возвращаемый тип является ссылкой на rvalue, поскольку move предназначен для преобразования чего-либо в rvalue.

Было бы неплохо узнать, верно ли мое понимание до сих пор.

С другой стороны, пример реализации std::forward в C++14 выглядит следующим образом.

template<typename T>
T&& forward(remove_reference_t<T>& param) {
  return static_cast<T&&>(param);
}

Мое понимание следующее:

  • T&&, тип возвращаемого значения должен быть пересылаемой/универсальной ссылкой, так как мы хотим, чтобы forward возвращал либо ссылку rvalue, либо ссылку lvalue, следовательно, здесь происходит вывод типа возвращаемого типа (в отличие от того, что происходит для move, где вывод типа происходит на стороне параметра) является ссылкой rvalue на любой аргумент типа шаблона, переданный в forward;
  • поскольку T кодирует lvalue/rvalue-ness фактического аргумента, который связывает параметр вызывающего абонента, который передается в качестве аргумента для forward, сам T может иметь значение actual_type& или actual_type, следовательно, T&& может быть либо ссылкой lvalue, либо ссылкой rvalue.
  • Тип param является ссылкой lvalue на тип T после того, как его выведенная ссылка удалена. На самом деле в std::forward вывод типа намеренно отключен, требуя, чтобы аргумент типа шаблона передавался явно.

Мои сомнения в следующем.

  • Два экземпляра forward (по два для каждого типа, для которого он вызывается на самом деле) отличаются только типом возвращаемого значения (ссылка на rvalue при передаче rvalue, ссылка на lvalue при передаче lvalue), поскольку в обоих случаях param имеет тип lvalue ссылка на не-const без ссылок T. Разве тип возвращаемого значения не учитывается при разрешении перегрузки? (Возможно, здесь я неправильно употребил слово «перегрузка».)
  • Поскольку тип param не является const ссылкой lvalue на T без ссылки, и поскольку ссылка lvalue должна быть to-const для привязки к rvalue, как param может привязываться к rvalue?

В качестве побочного вопроса:

  • можно ли использовать decltype(auto) для возвращаемого типа, как это делается для move?

person Enlico    schedule 03.08.2019    source источник
comment
Второй перегрузкой std::forward является forward(remove_reference_t<T>&& param), а remove_reference не удаляет квалификаторы cv.   -  person Piotr Skotnicki    schedule 03.08.2019
comment
@PiotrSkotnicki, теперь я вижу, что на самом деле есть две перегрузки std::forward. Я удивляюсь, почему в книге Мейерса упоминается только первое...   -  person Enlico    schedule 03.08.2019
comment
Вы забыли constexpr и noexcept.   -  person Deduplicator    schedule 03.08.2019
comment
@Deduplicator, эти два парня нужны, чтобы понять, как работает std::forward?   -  person Enlico    schedule 03.08.2019
comment
@EnricoMariaDeAngelis Только если вы хотите понять, на какие части они тоже влияют. Или, по крайней мере, не хотите производить ложное впечатление и не объяснять, что их не упомянули.   -  person Deduplicator    schedule 03.08.2019


Ответы (1)


forward по сути является механизмом сохранения категории стоимости при идеальной пересылке.

Рассмотрим простую функцию, которая пытается прозрачно вызвать функцию f, соблюдая категорию значений.

template <class T>
decltype(auto) g(T&& arg)
{
    return f(arg);
}

Здесь проблема в том, что выражение arg всегда является lvalue независимо от того, имеет ли arg ссылочный тип rvalue. Здесь пригодится forward:

template <class T>
decltype(auto) g(T&& arg)
{
    return f(forward<T>(arg));
}

Рассмотрим эталонную реализацию std::forward:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept
{
    return static_cast<T&&>(t);
}

template <class T>
constexpr T&& forward(remove_reference_t<T>&& t) noexcept
{
    static_assert(!std::is_lvalue_reference_v<T>);
    return static_cast<T&&>(t);
}

(Здесь можно использовать decltype(auto), потому что выводимый тип всегда будет T&&.)

Во всех следующих случаях вызывается первая перегрузка, поскольку выражение arg обозначает переменную и, следовательно, является lvalue:

  • Если g вызывается с неконстантным lvalue, то T выводится как неконстантный ссылочный тип lvalue. T&& совпадает с T, а forward<T>(arg) является неконстантным выражением lvalue. Поэтому f вызывается с неконстантным выражением lvalue.

  • Если g вызывается с константным lvalue, то T выводится как ссылочный тип const lvalue. T&& совпадает с T, а forward<T>(arg) является константным выражением lvalue. Поэтому f вызывается с выражением const lvalue.

  • Если g вызывается со значением r, то T выводится как нессылочный тип. T&& — это ссылочный тип rvalue, а forward<T>(arg) — это выражение rvalue. Поэтому f вызывается с выражением rvalue.

Во всех случаях соблюдается стоимостная категория.

Вторая перегрузка не используется при обычной идеальной переадресации. См. Какова цель перегрузки ссылки rvalue std::forward()? для ее использования.

person L. F.    schedule 04.08.2019
comment
Возможно, стоит добавить (напомнить), что существует три разных варианта T. (Кроме того, поскольку вопрос упоминает об этом, можно объяснить путаницу с типами возвращаемых значений.) - person Davis Herring; 04.08.2019
comment
Я еще не принял ответ, так как еще не понял (мой плохой). - person Enlico; 18.09.2019
comment
@EnricoMariaDeAngelis Может быть, я смогу вам помочь, если вы точно укажете, какую часть вы не понимаете. - person L. F.; 19.09.2019
comment
@ L.F., я в основном путаю rvalue/lvalue-ness аргумента для forward<T> и rvalue/lvalue-ness аргумента для его вызывающего. В самом деле, независимо от того, является ли аргумент g значением r или значением l, аргумент forward<T> является значением lvalue (arg), поэтому вторую перегрузку 'forward' никогда не следует вызывать (по крайней мере, в трех приведенных вами примерах, поскольку в у всех аргументов forward<T> есть имя, arg), я ошибаюсь? - person Enlico; 19.09.2019
comment
@EnricoMariaDeAngelis Точно, вторая перегрузка не используется при идеальной переадресации. - person L. F.; 20.09.2019
comment
@L.F., тогда мне нужно разъяснение по поводу вашего третьего Если g вызывается с, так как там вы говорите, что выбрана вторая перегрузка. - person Enlico; 28.09.2019
comment
@EnricoMariaDeAngelis Плохо, я хотел сказать, что forward вызывается со значением r, которое не имеет значения. Я обновил ответ, надеюсь, он понятнее - person L. F.; 28.09.2019