Stack Overflow на русском Asked by pier_nasos on December 19, 2021
#include <iostream>
#include <string>
#include <memory>
template<typename T, typename Arg>
std::shared_ptr<T> factory(const Arg& arg) {
std::cout << "factory1n";
return std::shared_ptr<T>(new T(arg));
}
template<typename T, typename Arg>
std::shared_ptr<T> factory(Arg&& arg) {
std::cout << "factory2n";
return std::shared_ptr<T>(new T(std::move(arg)));
}
int main()
{
auto sp1 = factory<std::string>(std::string("AAAAA"));
std::string s("BBBBBBB");
auto sp2 = factory<std::string>(s);
}
output:
factory2
factory2
Шаблонные функции взяты из примеров ю-туб лекции по с++11. Второй вызов factory должен был привести к вызову второй функции, то есть, в консоли ожидалось:
factory2
factory1
Почему всегда вызывается factory2? Как вызвать factory1?
TL;DR:
У вас во второй функции параметр - не обычная rvalue-ссылка, а так называемая пробрасывающая ссылка1, которая одинаково хорошо обрабатывает и lvalue, и rvalue.
В этом случае она сработала как std::string &
. А этот вариант подходит лучше, чем const std::string &
, так что вызывается вторая функция.
1 — Англ. "forwarding reference", иногда еще переводят как "передаваемая ссылка". Менее распространено другое название, "universal reference" - универсальная ссылка.
Что еще за пробрасывающие ссылки
Их иногда называют "универсальными" - потому их одинаково хорошо можно инициализировать выражениями не только любого типа, но и любой категории: lvalue или rvalue. С информацией о категории дальше можно делать что угодно, но обычно ее используют, чтобы передать ("пробросить") ссылку куда-то еще с сохранением оригинальной категории. Такая передача называется perfect forwarding - "идеальная передача".
По сути, "пробрасывающая ссылка" - это rvalue-ссылка (&&
), но не на любой тип, а на параметр шаблона, который при вызове функции вы позволили компилятору определить за вас. (Иначе особые свойства пробрасывющей ссылки пропадают, и она превращается в обычную rvalue-ссылку.)
Также, вместо параметра шаблона подходит auto
- если ссылка является отдельной переменной или параметром лямбды. (В C++20 добавился краткий способ записи шаблонов функций, позволяющий использовать auto
также в параметрах обычных функций.)
Почему это работает
Почему такая ссылка принимает и lvalue тоже, хотя, казалось бы, должна работать только с rvalue?
Сначала нужно рассказать про...
Правила схлопывания ссылок
Думаю понятно, что так писать нельзя: int & &&
- ссылок на ссылки не бывает, компилятор запрещает их создавать.
Однако, что есть написать так?
using A = int &;
A &&my_reference = что-нибудь;
Этот вариант скомпилируется.
my_reference
будет иметь тип int &
. Почему? Из-за этих самых правил схлопывания ссылок.
Если вы пытаетесь создать ссылку на ссылку (не напрямую, ведь это ошибка, а через using
и или параметры шаблонов), то вид ссылки, которая получится: на lvalue или на rvalue - определяется по этим правилам:
& + & -> &
&& + & -> &
& + && -> &
&& + && -> &&
Почему именно так? Понятия не имею; слышал, что эти правила специально подгоняли под пробрасывющие ссылки... Подробнее ниже.
Определение аргумента шаблона по пробрасывющей ссылке
Казалось бы, какой смысл в правилах схлопывания ссылок? Вот какой:
Если вы пытаетесь передать в пробрасывющую ссылку rvalue, то аргумент шаблона определяется компилятором по обычным правилам:
template <typename T> void foo(T &&) {}
foo(1); // T = int
С этим все понятно, и никаких вопросов не возникает.
Однако, если передать lvalue, вот так:
template <typename T> void foo(T &&) {}
foo(1); // T = int
int x = 1;
foo(x); // T = int &
То шаблонный аргумент будет определен как lvalue-ссылка. Далее, по правилам схлопывания ссылок весь параметр функции становится lvalue-ссылкой.
Если передавать константный объект, то тип будет определен, соответственно, как const T
или const T &
.
Теперь должно быть понятно, почему в вашем случае оба раза вызывается вторая перегрузка.
В этом примере:
std::string s("BBBBBBB"); factory<std::string>(s);
Параметр функции, благодаря пробрасывающей ссылке, получит тип
std::string &
.А это более подходящий тип, чем
const std::string &
, так что выбирается вариант с пробрасывающей ссылкой.
Как пользоваться пробрасывающими ссылками
Все удобство в том, что теперь мы внутри шаблона можем определить, что нам передали - lvalue или rvalue. И на основе этого решить, например, делать std::move
или нет.
Как именно определить? По тому, определился ли параметр шаблона как ссылка:
template <typename T> void foo(T &&)
{
if (std::is_reference_v<T>) // Или is_lvalue_reference, без разницы.
std::cout << "lvaluen";
else
std::cout << "rvaluen";
}
Для справки: Можно подумать, что проверка на lvalue/rvalue - бесполезное занятие, и попробовать написать так:
template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg) { return std::shared_ptr<T>(new T(arg)); }
Но это не сработает. Rvalue-ссылки, как и любые переменные, сами являются lvalue (хотя и ссылаются на rvalue), и к ним нужно применять
std::move
. Такой вот курьез языка.
Ладно, а как на основе этого сделать условный std::move
? Наивный вариант выглядит так:
template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
if (std::is_reference_v<T>)
return std::shared_ptr<T>(new T(arg));
else
return std::shared_ptr<T>(new T(std::move(arg)));
}
Однако, есть вариант проще. В стандартной библиотеке уже есть все необходимое:
template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
std::forward
- это, по сути, условный std::move
. Если его шаблонный параметр - это lvalue ссылка, то он ничего не делает. А иначе он работает как std::move
.
Если не нравится стандартная библиотека, можно даже так:
template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
return std::shared_ptr<T>(new T((Arg &&)arg));
}
Эффект точно такой же. Это работает из-за правил схлопывания ссылок, описанных выше.
Итог
Таким образом, вам не нужно две перегрузки.
Достаточно одной всеядной:
template <typename T, typename Arg> std::shared_ptr<T> factory(Arg &&arg)
{
return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
Answered by HolyBlackCat on December 19, 2021
factory(Arg&& arg)
это «всеядный» вызов, который в обоих случаях лучше первого варианта. Для второго случая, чтобы вызвать первый вариант, нужно добавить к аргументу const
, а для второго варианта — ничего. Поэтому второй побеждает. Если Вы определите свой объект так:
const std::string s("BBBBBBB");
То победит первый вариант.
Answered by ixSci on December 19, 2021
Get help from others!
Recent Answers
Recent Questions
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP