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

Универсальные ссылки (то есть «прямые ссылки», 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: примечание: функция-кандидат [с T = foo, A = int] not
жизнеспособный: нет известного преобразования из ‘struct foo< int> ‘to’ foo< int> &&’
за 1-й аргумент

decltype (авто) f (T)< A> && т)

#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++Прямые ссылки с шаблонными типами?

11

Решение

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

int a = 42;
f(a);

затем f должен быть в состоянии принять такое значение. Это тот случай, когда первый параметр 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

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

  • 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> && не нуждается в свертывании и остается тем, что есть: ссылка на значение. И, конечно, вы не можете привязать lvalue к ссылке на 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, Вы, конечно, могли бы обеспечить перегрузки, но это как-то не позволило бы достичь идеальной пересылки, я думаю.


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

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,

7

Другие решения

Почему второй пример не работает, как ожидалось?

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

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

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

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

экспедиционная ссылка это значение
ссылка на резюме параметр шаблона. Если P является ссылкой для пересылки, а аргумент является
lvalue, тип «lvalue ссылка на A» используется вместо A для вывода типа.

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

2

Что касается преодоления проблемы, я думаю, что более или менее эквивалентным способом было бы вывести ее с помощью прямой ссылки и инициировать сравнение с 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>());
}
0

Недостаточно иметь вывод типа. Форма объявления типа должна быть именно так 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

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

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

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

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), но я не могу ответить на эту часть. 🙂

0
По вопросам рекламы [email protected]