#c #templates #c 11 #decltype #perfect-forwarding
#c #шаблоны #c 11 #decltype #идеальная пересылка
Вопрос:
Я очень часто вижу пример этой формы:
template <typename T, typename U>
auto add(Tamp;amp; t, Uamp;amp; u) -> decltype(std::forward<T>(t) std::forward<U>(u))
{
return std::forward<T>(t) std::forward<U>(u);
}
но я осмелюсь сказать, что это лучший, более правильный способ:
template <typename T, typename U>
auto add(Tamp;amp; t, Uamp;amp; u) -> decltype(t u)//no forwarding here
{
return std::forward<T>(t) std::forward<U>(u);
}
Почему? Прежде всего, decltype в этом примере должен только определять тип возвращаемого значения, поэтому (t u) не является типом возвращаемого значения (std::forward(t) std::forward(u)), второй код, сгенерированный двумя версиями, идентичен, а третий decltype (u t) более прямой и точно выражает намерения программиста, не раскрывая «внутренности» реализации.
Каково ваше мнение по этому вопросу?
Ответ №1:
Первая версия более правильная, потому что она точно соответствует тому, что должно возвращать тело функции. Как уже указывалось, просто нет гарантии, что
decltype(std::forward<T>(t) std::forward<U>(u))
будет того же типа, что и
decltype(t u)
Возможно, это довольно сложный случай, но «правильный» способ — использовать std::forward .
Ответ №2:
В общем, я не могу придумать разумный вариант использования, где будет разница, но я предполагаю, что вы могли бы найти какой-то случай, когда операция имеет разные реализации для rvalue-ссылок и rvalues, и правила языка не предписывают, что возвращаемый тип для разных перегрузок должен быть одинаковым (даже если это диктует здравый смысл).
Итак, в общем случае разницы не будет, а в случае, когда различия есть, что ж, в этих случаях требуется дополнительная осторожность и внимание для решения гораздо более серьезных проблем, чем сам шаблон…
// sick corner case:
struct type {};
int operator ( typeamp;amp; lhs, typeamp;amp; rhs );
double operator ( type const amp; lhs, type const amp; rhs );
Я могу подумать о ситуациях, когда вы хотели бы предложить различные перегрузки для ссылок на значения (рассмотрим некоторую реализацию списка, который предлагает operator
в качестве конкатенации, тогда перегрузка ссылками на значения могла бы избежать затрат на копирование, просто изменив указатели и оставив списки аргументов пустыми), но было бы крайне запутанно, если бы тип результата зависел от l / rvalue-ness аргументов.
Комментарии:
1. чем больше я использую decltype, тем больше я убежден, что он должен работать с типами, а не с объектами. Потому что более естественно думать о том, что является результатом добавления двух типов вместо двух объектов этих типов.
2. @Мы ничего не можем сделать: я не совсем согласен с этим, в использовании объектов вместо типов есть явное преимущество: компилятор выполнит любое требуемое преобразование аргументов:
template <typename T, typename U> auto sum( T lhs, U rhs ) -> decltype( lhs rhs ) { return lhs rhs; }
преобразует в большее изT
иU
для арифметических типов.template <typename T> auto foo( T x ) -> decltype( foo_(x) ) { return foo_(x); }
выполнит любое неявное преобразование, которое может потребоваться, от фактическогоT
к наилучшей перегрузкеfoo_
из доступных…3. Я предполагаю, что вы также могли бы подумать, что это
decltype( foo( int ) )
можно использовать в грамматике, но это создало бы первый случайid( type )
, который не является идеальным совпадением (во всех других случаях преобразования изint
не может быть) и, вероятно, привело бы к путанице. О какомfoo
из них вы говорите? У меня есть толькоfoo(doble)
иfoo(string)
!
Ответ №3:
Внутри decltype(t u)
переменные t
и u
уже не являются ссылками на rvalue, они будут обрабатываться как простые ссылки на lvalue, следовательно, вам нужно дополнительное std::forward
. (По крайней мере, я так это понимаю. Хотя это может быть и неправильно.)
Комментарии:
1. но это только для того, чтобы вывести тип retur, ничего больше, вы ничего не делаете с этими переменными в decltype.
2. @There: Это все еще может быть важно, поскольку может возникнуть особая перегрузка
operator
ссылок на rvalue.