Невозможность создания шаблонов функций из-за универсальной (прямой) ссылки на шаблонный тип

Универсальные ссылки (т.е. "прямые ссылки", стандартное имя c++) и безупречная переадресация в c++11, c++14, и далее имеют много важных преимуществ; см. здесь и здесь.

В статье Скотта Мейерса, упомянутой выше (ссылка), в качестве эмпирического правила указано, что:

Если переменная или параметр объявлены как имеющие тип T && для некоторого выведенного типа T, эта переменная или параметр является универсальной ссылкой.

Пример 1

В самом деле, используя clang++, мы видим, что следующий фрагмент кода успешно скомпилируется с помощью -std=c++14:

#include <utility>

template <typename T>
decltype(auto) f(T && t)
{
    return std::forward<T>(t);
}

int        x1 = 1;
int const  x2 = 1;
int&       x3 = x1;
int const& x4 = x2;

// all calls to `f` result in a successful
// binding of T&& to the required types
auto r1 = f (x1);    // various lvalues okay, as expected
auto r2 = f (x2);    // ...
auto r3 = f (x3);
auto r4 = f (x4);
auto r5 = f (int()); // rvalues okay, as expected

Учитывая любое описание универсальных ссылок (прямых ссылок) и вывода типа (см., например, это объяснение), понятно, почему выше работ. Хотя, по тому же объяснению, не совсем понятно, почему нижеследующее не работает.

(не удалось) Пример 2

Этот вопрос касается той же проблемы. Однако предоставленные ответы не объясняют, почему шаблонные типы не классифицируются как "выведенные".

То, что я собираюсь показать (по-видимому), удовлетворяет требованию, указанному выше Мейерсом. Однако следующий код обрезал сбой для компиляции, вызвав ошибку (среди прочего, для каждого вызова f):

test.cpp: 23: 11: ошибка: нет соответствующей функции для вызова 'f'

auto r1 = f (x1);

test.cpp: 5: 16: note: функция-кандидат [с T = foo, A = int] not жизнеспособный: никакого известного преобразования из 'struct foo <int> 'to' foo <int> & & для 1-го аргумента

decltype (auto) f (T < A && t)

#include <utility>

//
// It **seems** that the templated type T<A> should
// behave the same as an bare type T with respect to
// universal references, but this is not the case.
//
template <template <typename> typename T, typename A>
decltype(auto) f (T<A> && t)
{
    return std::forward<T<A>> (t);
}

template <typename A>
struct foo
{
    A bar;
};

struct foo<int>        x1 { .bar = 1 };
struct foo<int> const  x2 { .bar = 1 };
struct foo<int> &      x3 = x1;
struct foo<int> const& x4 = x2;

// all calls to `f` **fail** to compile due
// to **unsuccessful** binding of T&& to the required types
auto r1 = f (x1);
auto r2 = f (x2);
auto r3 = f (x3);
auto r4 = f (x4);
auto r5 = f (foo<int> {1}); // only rvalue works

В контексте, поскольку вывод T<A> параметра f , конечно, объявление параметра T<A>&& t будет вести себя как универсальная ссылка (прямая ссылка).

Пример 3 (для ясности при описании проблемы)

Позвольте мне подчеркнуть следующее: сбой кода в Example 2 для компиляции - не из-за того, что struct foo<> является шаблоном. Ошибка, по-видимому, является причиной только объявлением параметра f в качестве шаблонного типа.

Рассмотрим следующий вариант предыдущего кода, который теперь выполняет:

#include <utility>

//
// If we re-declare `f` as before, where `T` is no longer a
// templated type parameter, our code works once more.
//
template <typename T>
decltype(auto) f (T && t)
{
    return std::forward<T> (t);
}

//
// Notice, `struct foo<>` is **still** a templated type.
//
template <typename A>
struct foo
{
    A bar;
};

struct foo<int>        x1 { .bar = 1 };
struct foo<int> const  x2 { .bar = 1 };
struct foo<int> &      x3 = x1;
struct foo<int> const& x4 = x2;

// all calls to `f` (again) result in
// a successful binding of T&& to the required types
auto r1 = f (x1);
auto r2 = f (x2);
auto r3 = f (x3);
auto r4 = f (x4);

Для меня удивительно, что это простое изменение полностью изменяет поведение вывода типа для параметра типа шаблона f.

Вопросы:

Почему второй пример работает не так, как ожидалось? Существуют ли методы преодоления этой проблемы с шаблонами в c++11/14? Существуют ли хорошо известные, существующие кодовые базы (в дикой природе), успешно использующие прямые ссылки c++ с шаблонами?

Ответ 1

Когда вы вызываете некоторую функцию f с некоторым значением lvalue:

int a = 42;
f(a);

Тогда f должен иметь возможность принимать такое значение lvalue. Это тот случай, когда первый параметр f является ссылочным типом (lvalue), или когда он не является ссылкой вообще:

auto f(int &);
auto f(int); // assuming a working copy constructor

Это не будет работать, если параметр является ссылкой rvalue:

auto f(int &&); // error

Теперь, когда вы определяете функцию с ссылкой пересылки как первый параметр, как в первом и третьем примерах...

template<typename T>
auto f(T&&); // Showing only declaration

... и вы на самом деле вызываете эту функцию с lvalue, вывод типа шаблона превращает T в ссылку (lvalue) (что это можно увидеть в приведенном ниже примере кода):

auto f(int & &&); // Think of it like that

Несомненно, слишком много ссылок приведено выше. Таким образом, С++ имеет правила сворачивания, которые на самом деле довольно просты:

  • T& & становится T&
  • T& && становится T&
  • T&& & становится T&
  • T&& && становится T&&

Благодаря второму правилу "эффективный" тип первого параметра f является ссылкой на lvalue, поэтому вы можете привязать его lvalue к нему.

Теперь, когда вы определяете функцию g как...

template<template<class> class T, typename A>
auto g(T<A>&&);

Тогда независимо от того, что, параметр шаблона должен, превратите T в шаблон, не тип. В конце концов, вы точно указали, что при объявлении параметра шаблона template<class> class вместо typename. (Это важное различие, foo в вашем примере не тип, это шаблон... который вы можете видеть как функцию уровня уровня, но вернуться к теме)

Теперь T является своего рода шаблоном. У вас нет ссылки на шаблон. Ссылка (тип) построена из (возможно, неполного) типа. Поэтому независимо от того, что T<A> (который является типом, но не параметром шаблона, который может быть выведен) не превратится в ссылку (lvalue), что означает, что T<A> && не нуждается в коллапсе и остается тем, что он is: Ссылка rvalue. И, конечно, вы не можете привязать lvalue к ссылке rvalue.

Но если вы передадите ему значение rvalue, тогда даже g будет работать.

Все вышесказанное можно увидеть в следующем примере:

template<typename X>
struct thing {
};
template<typename T>
decltype (auto) f(T&& t) {
 if (std::is_same<typename std::remove_reference<T>::type, T>::value) {
  cout << "not ";
 }
 cout << "a reference" << endl;
 return std::forward<T>(t);
}
template<
 template<class> class T,
 typename A>
decltype (auto) g(T<A>&& t) {
 return std::forward<T<A>>(t);
}
int main(int, char**) {
 thing<int> it {};

 f(thing<int> {}); // "not a reference"

 f(it);            // "a reference"
 // T = thing<int> &
 // T&& = thing<int>& && = thing<int>&

 g(thing<int> {}); // works

 //g(it);
 // T = thing
 // A = int
 // T<A>&& = thing<int>&&

 return 0;
}

(Живой здесь)

Относительно того, как можно "преодолеть" это: вы не можете. По крайней мере, не так, как вам кажется, потому что естественное решение является третьим примером, который вы предоставляете: поскольку вы не знаете переданный тип (это ссылка lvalue, ссылка rvalue или ссылка на все?), вы должны сохранить его как общее, как T. Разумеется, вы можете обеспечить перегрузку, но это как-то победит цель идеальной пересылки, я думаю.


Hm, получается, что вы на самом деле можете преодолеть это, используя некоторый класс признаков:

template<typename> struct traits {};
template<
 template<class>class T,
 typename A>
struct traits<T<A>> {
 using param = A;
 template<typename X>
 using templ = T<X>;
};

Затем вы можете извлечь как шаблон, так и тип, с которым был создан шаблон внутри функции:

template<typename Y>
decltype (auto) g(Y&& t) {
 // Needs some manual work, but well ...
 using trait = traits<typename std::remove_reference<Y>::type>;
 using A = typename trait::param;
 using T = trait::template templ
 // using it
 T<A> copy{t};
 A data;
 return std::forward<Y>(t);
}

(Живой здесь)


[...] можете ли вы объяснить, почему это не универсальная ссылка? какова будет опасность или ловушка, или это слишком сложно реализовать? Я искренне заинтересован.

T<A>&& не является универсальной ссылкой, потому что T<A> не является параметром шаблона. Он (после вычета как T, так и A) простого (фиксированного/не общего) типа.

Серьезной ошибкой в ​​создании этой ссылки для пересылки было бы то, что вы больше не могли бы выразить текущее значение T<A>&&: Ссылка rvalue на некоторый тип, построенный из шаблона T с параметром A.

Ответ 2

Почему второй пример не работает должным образом?

У вас есть две подписи:

template <typename T>                                                                                                                   
decltype(auto) f (T&& );

template <template <typename> typename T, typename A>                                                                                                                   
decltype(auto) f2 (T<A>&& );

f берет ссылку на пересылку, но f2 нет. Конкретное правило, из [temp.deduct.call], смелое внимание мое:

Ссылка на пересылку представляет собой rvalue ссылку на cv-unqualified параметр шаблона. Если P является ссылкой пересылки, и аргумент является lvalue, тип "lvalue reference to A" используется вместо A для вывода типа.

С f аргумент является ссылкой rvalue на параметр шаблона (T). Но с f2, T<A> не является параметром шаблона. Таким образом, эта функция просто принимает в качестве аргумента ссылку rvalue на T<A>. Вызовы не скомпилируются, потому что все ваши аргументы являются lvalues, и этот случай не имеет особого исключения для вызова с lvalue.

Ответ 3

Что касается преодоления проблемы, я думаю, более или менее эквивалентный способ заключается в том, чтобы вывести ее с помощью прямой ссылки и запустить сравнение с T<A> вручную.

template<typename T>
class X;

template<template<typename> class T, typename A>
class X<T<A>> {
public:
   using type = A;

   template<typename _>
   using template_ = T<_>;
};

template<typename T, typename R>
struct copyref {
  using type = T;
};
template<typename T, typename R>
struct copyref<T, R&> {
   using type = T&;
};
template<typename T, typename R>
struct copyref<T, R&&> {
   using type = T&&;
};

template <typename U, typename XX = X<std::decay_t<U>>, 
          typename = typename XX::type >                                                                                                                   
decltype(auto) f (U && t)                                                                                                               
{                                                                                                                                      
    return std::forward<
       typename copyref<
         typename XX::template template_<typename XX::type>, U
       >::type>(t);
}

Если вы действительно не хотите T<A>, но конкретный тип, лучший способ - использовать std::enable_if_t<std::is_same_v<std::decay_t<U>, SpecificType>>, что намного проще, я думаю.

int main() {
    static_assert(std::is_same<decltype(f(std::declval<X<int>&>())), 
                  X<int>&>::value, "wrong");
    static_assert(std::is_same<decltype(f(std::declval<X<int>>())), 
                  X<int>&&>::value, "wrong");
    // compile error
    //f(std::declval<int>());
}

Ответ 4

Недостаточно иметь вычет типа. Форма объявления типа должна быть точно T&& (ссылка rvalue только на параметр шаблона). Если это не (или нет вывода типа), параметр является ссылкой rvalue. Если аргумент равен lvalue, он не будет компилироваться. Так как T<A>&& не имеет такой формы, f (T<A> && t) не может принять значение lvalue (как ссылку на lvalue), и вы получите ошибку. Если вы считаете, что это требует слишком большой общности, считайте, что простое определение const также нарушает его:

template<typename T>
void f(const T&& param); // rvalue reference because of const

(отбрасывая относительную бесполезность ссылки const rvalue)

Правила ссылочного коллапса просто не срабатывают, если не используется самая общая форма T&&. Без возможности для f распознавать аргумент lvalue был передан и обрабатывать параметр как ссылку lvalue, не происходит никакого свертывания ссылки (т.е. Сбрасывание T& && в T& не может произойти, и это просто T<something>&&, rvalue ref к шаблонизированному типу). Необходимый механизм функции для определения того, передается ли значение rvalue или lvalue в качестве аргумента, кодируется в параметре выведенного шаблона. Однако эта кодировка встречается только для универсального ссылочного параметра, строго определенного.

Почему этот уровень общности необходим (помимо того, что он является правилом)? Без этого конкретного формата определения универсальные ссылки не могут быть супер-жадными функциями, которые создают экземпляры любого типа аргументов... поскольку они предназначены для того, чтобы быть. Даниэль ответит, я думаю: Предположим, вы хотите определить функцию с регулярной ссылкой rvalue на параметр templated type, T<A>&& (то есть, который не принимает аргумент lvalue). Если следующий синтаксис рассматривался как универсальная ссылка, то как бы вы изменили его, чтобы указать регулярную ссылку rvalue?

template <template <typename> typename T, typename A>
decltype(auto) f (T<A> && t) // rv-ref - how else would you exclude lvalue arguments?

Должен быть способ явно определить параметр как ссылку rvalue для исключения аргументов lvalue. Эта аргументация, похоже, применима к другим типам параметров, включая квалификацию cv.

Кроме того, есть способы обойти это (см. черты и SFINAE), но я не могу ответить на эту часть.:)