Это универсальная ссылка? Имеет ли здесь смысл std::forward?

Рассмотрим этот фрагмент кода, в котором используется общепринятая идиома: шаблон функции создает экземпляр шаблона класса, специализированного на выведенном типе, как видно из std::make_unique и std::make_tuple, например:

template <typename T>
struct foo
{
    std::decay_t<T> v_;
    foo(T&& v) : v_(std::forward<T>(v)) {}
};

template <typename U>
foo<U> make_foo(U&& v)
{
    return { std::forward<U>(v) };
}

В контексте «универсальных ссылок» Скотта Мейерса аргумент make_foo является универсальной ссылкой, поскольку его тип — U&&, где выводится U. Аргумент конструктора foo не является универсальной ссылкой, потому что, хотя его тип T&&, T (в общем случае) не выводится.

Но в случае, когда конструктор foo вызывается make_foo, мне кажется, что имеет смысл рассматривать аргумент конструктора foo как универсальную ссылку, поскольку T был выведен шаблоном функции make_foo. . Будут применяться одни и те же правила свертывания ссылок, так что тип v будет одинаковым в обеих функциях. В этом случае можно сказать, что и T, и U были выведены.

Итак, мой вопрос двоякий:

  • Имеет ли смысл думать об аргументе конструктора foo как об универсальной ссылке в ограниченных случаях, когда T выводится вызывающей стороной в контексте универсальной ссылки, как в моем примере?
  • В моем примере оба варианта использования std::forward разумны?

person Oktalist    schedule 30.06.2014    source источник


Ответы (2)


make_foo находится на том же уровне, что и "правильно", а foo - нет. Конструктор foo в настоящее время только принимает невыведенный T &&, и пересылка туда, вероятно, не то, что вы имеете в виду (но см. комментарий @nosid). В общем, foo должен принимать параметр типа, иметь шаблонный конструктор, а функция maker должна выполнять затухание:

template <typename T>
struct foo
{
    T v_;

    template <typename U>
    foo(U && u) : v_(std::forward<U>(u)) { }
};

template <typename U>
foo<typename std::decay<U>::type> make_foo(U && u)
{
    return foo<typename std::decay<U>::type>(std::forward<U>(u));
}

В C++14 функцию maker написать немного проще:

template <typename U>
auto make_foo(U && u)
{ return foo<std::decay_t<U>>(std::forward<U>(u)); }

Поскольку ваш код написан сейчас, int a; make_foo(a); создаст объект типа foo<int &>. Это будет внутренне хранить int, но его конструктор будет принимать только аргумент int &. Напротив, make_foo(std::move(a)) создаст foo<int>.

Итак, как вы написали, аргумент шаблона класса определяет подпись конструктора. ( std::forward<T>(v) все еще имеет смысл в извращенном виде (спасибо @nodis за указание на это), но это определенно не «пересылка».)

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

person Kerrek SB    schedule 30.06.2014
comment
в качестве альтернативы используйте std::move(v) внутри исходного конструктора - person TemplateRex; 30.06.2014
comment
@TemplateRex: я не хочу называть это альтернативой, поскольку это явно не то, чего хотел OP (поскольку в этом случае производитель не имел бы смысла принимать ссылки lvalue). - person Kerrek SB; 30.06.2014
comment
Конструктор foo<int&> принимает ссылку lvalue. В этом случае может иметь смысл использование std::forward<T>. - person nosid; 30.06.2014
comment
@nosid: А, хорошо замечено. Совершенно верно. Хорошо, я предполагаю, что ОП этого не хочет (поскольку ОП хочет, чтобы конструктор foo был универсальной ссылкой), но это возможно. (Также интересно рассмотреть foo<int&&>.) - person Kerrek SB; 30.06.2014
comment
В случае С++ 11 вам нужно return foo<...>(...) или просто return { ... } (как в вопросе) не работает? (что делает преимущество версии C++14 минимальным) - person leemes; 30.06.2014
comment
Разве не существует правило, согласно которому конструкторы шаблонов никогда не создаются для копирования и перемещения? - person Deduplicator; 30.06.2014
comment
@Deduplicator: конструктор копирования/перемещения никогда не является шаблоном. - person Kerrek SB; 30.06.2014
comment
Никогда не говорил, что хочу, чтобы конструктор foo вообще принимал универсальную ссылку (хотя так лучше). Ваш ответ демонстрирует правильный способ сделать то, что я хотел, но комментарий nosid соответствовал моему первоначальному намерению. - person Oktalist; 30.06.2014
comment
@Oktalist: Да, я расширил это. Вы могли сделать это, но это было бы очень необычно. В лучшем случае вы могли бы сделать этот конструктор закрытым и другом функции maker, но что действительно странно, так это то, как ваша функция maker создает непредсказуемые типы результатов. - person Kerrek SB; 30.06.2014
comment
@Oktalist: Например, представьте себе такой код, как auto x = make_foo(f());. Позже кто-то говорит: «О, мне может понадобиться f()», и меняет его на auto const & y = f(); /* ... */ auto x = make_foo(y);. Это просто зло :-) Итак, моя главная мысль заключается в том, что функция-создатель должна разрушать тип, если она выводит тип (или принимает аргумент невыведенного типа, например make_shared); но никогда не используйте его необработанный выведенный тип аргумента для определения типа результата. - person Kerrek SB; 30.06.2014
comment
@KerrekSB Я не думаю, что дело только в типе, но и в категориях значений. make_foo сохранит категории значений (и типы) аргументов и идеально перенаправит их. Тогда в конструкторе foo аргументом всегда будет ссылка lvalue + rvalue/lvalue, хотя может потребоваться rvalue. Другими словами, форвард foo ctor не является идеальным форвардом, поскольку тип не выводится (со всеми последствиями, о которых вы, ребята, упомянули). Кстати, отличный вопрос. - person KeyC0de; 01.10.2018

Не существует формального определения «универсальной ссылки», но я бы определил его как:

Универсальная ссылка – это параметр шаблона функции с типом [template-parameter] &&, предназначенный для того, чтобы параметр шаблона можно было вывести из аргумента функции, и аргумент будет передан либо по lvalue, ссылка или ссылка rvalue в зависимости от ситуации.

Так что по этому определению нет, параметр T&& v в конструкторе foo не является универсальной ссылкой.

Тем не менее, весь смысл фразы «универсальная ссылка» состоит в том, чтобы предоставить нам, людям, модель или шаблон, о которых можно думать при проектировании, чтении и понимании кода. И разумно и полезно сказать, что «Когда make_foo вызывает конструктор foo<U>, параметр шаблона T был выведен из аргумента make_foo таким образом, что позволяет параметру конструктора T&& v быть либо ссылкой на lvalue, либо ссылкой на rvalue как подходящее." Это достаточно близко к той же концепции, что я мог бы перейти к утверждению: «Когда make_foo вызывает конструктор foo<U>, параметр конструктора T&& v по сути является универсальной ссылкой».

Да, оба варианта использования std::forward будут делать то, что вы хотите здесь, позволяя элементу v_ перемещаться из аргумента make_foo, если это возможно, или копировать в противном случае. Но то, что make_foo(my_str) возвращает foo<std::string&>, а не foo<std::string>, которое содержит копию my_str, довольно удивительно....

person aschepler    schedule 30.06.2014