Я читал Страница cppreference на Ограничениях и заметил этот пример:
// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
Я озадачен, почему они используют std::forward
, Некоторые пытаются поддерживать ссылочные типы в параметрах шаблона? Разве мы не хотим звонить swap
со значениями, и не будет forward
выражения быть значениями, когда T
а также U
такое скалярные (не ссылочные) типы?
Например, я ожидаю, что эта программа потерпит неудачу, учитывая их Swappable
реализация:
#include <utility>
// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
class MyType {};
void swap(MyType&, MyType&) {}
void f(Swappable& x) {}
int main()
{
MyType x;
f(x);
}
К сожалению, g ++ 7.1.0 дает мне внутренняя ошибка компилятора, который не проливает много света на это.
Здесь оба T
а также U
должно быть MyType
, а также std::forward<T>(t)
должен вернуться MyType&&
, который не может быть передан моему swap
функция.
Это реализация Swappable
неправильно? Я что-то пропустил?
Разве мы не хотим вызывать своп с lvalues […]
Это очень хороший вопрос. В частности, вопрос разработки API: какое значение или значения должен придавать разработчик библиотеки концепций параметрам своих концепций?
Краткий обзор требований Swappable. То есть фактические требования, которые уже присутствуют в сегодняшнем Стандарте и были здесь еще до концептуального облегчения:
[…]
- Объект
t
является можно заменить объектu
если и только если:
- […] Выражения
swap(t, u)
а такжеswap(u, t)
действительны […]Rvalue или lvalue
t
можно заменить, если и только если t можно заменить на любое значение rvalue или lvalue, соответственно, типаT
,
(Отрывки из Swappable
требования [swappable.requirements] чтобы сократить множество несущественных деталей.)
Вы это поймали? Первый бит дает требования, которые соответствуют вашим ожиданиям. Очень просто превратить в реальную концепцию † тоже:
†: пока мы готовы игнорировать тонну деталей, которые находятся за пределами нашей компетенции
template<typename Lhs, typename Rhs = Lhs>
concept bool FirstKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
swap(lhs, rhs);
swap(rhs, lhs);
};
Теперь, что очень важно, мы должны немедленно заметить, что эта концепция поддерживает ссылочные переменные прямо из коробки:
int&& a_rref = 0;
int&& b_rref = 0;
// valid...
using std::swap;
swap(a_rref, b_rref);
// ...which is reflected here
static_assert( FirstKindOfSwappable<int&&> );
(Теперь технически Стандарт говорил с точки зрения объектов, ссылки на которые нет. Поскольку ссылки не только относятся к объектам или функции но призваны прозрачно отстаивать их, мы на самом деле предоставили очень желательную функцию. Практически говоря, мы сейчас работаем с точки зрения переменные, не только объекты.)
Здесь очень важная связь: int&&
является объявленным типом наших переменных, а также фактическим аргументом, передаваемым в концепцию, которая, в свою очередь, снова становится объявленным типом нашего lhs
а также rhs
требует параметров. Имейте это в виду, когда мы копаем глубже.
А что насчет второго бита, в котором упоминаются lvalues и rvalues? Ну, здесь мы больше не имеем дело с переменными, а вместо этого с точки зрения выражения. Можем ли мы написать концепцию для этого? Ну, есть определенная кодировка типа выражения, которую мы можем использовать. А именно тот, который используется decltype
так же как std::declval
в другом направлении. Это приводит нас к:
template<typenaome Lhs, typename Rhs = Lhs>
concept bool SecondKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs));
swap(std::forward<Rhs>(rhs), std::forward<Lhs>(lhs));
// another way to express the first requirement
swap(std::declval<Lhs>(), std::declval<Rhs>());
};
С чем ты столкнулся! И, как вы узнали, концепция должна использоваться по-другому:
// not valid
//swap(0, 0);
// ^- rvalue expression of type int
// decltype( (0) ) => int&&
static_assert( !SecondKindOfSwappable<int&&> );
// same effect because the expression-decltype/std::declval encoding
// cannot properly tell apart prvalues and xvalues
static_assert( !SecondKindOfSwappable<int> );
int a = 0, b = 0;
swap(a, b);
// ^- lvalue expression of type int
// decltype( (a) ) => int&
static_assert( SecondKindOfSwappable<int&> );
Если вы обнаружите, что это неочевидно, взгляните на соединение в игре на этот раз: у нас есть lvalue-выражение типа int
, который становится закодированным как int&
аргумент концепции, которая восстанавливается до выражения в нашем ограничении std::declval<int&>()
, Или более окольным путем, std::forward<int&>(lhs)
,
То, что появляется в записи cppreference, является сводкой Swappable
Концепция, указанная в диапазонах TS. Если бы я угадал, я бы сказал, что ТС Рэнджес решил дать Swappable
Параметры для выражений по следующим причинам:
мы можем написать SecondKindOfSwappable
с точки зрения FirstKindOfSwappable
как указано в следующем около:
template<typename Lhs, typename Rhs = Lhs>
concept bool FirstKindOfSwappable = SecondKindOfSwappable<Lhs&, Rhs&>;
Этот рецепт может применяться во многих, но не во всех случаях, что иногда позволяет выразить концепцию, параметризованную для типов переменных, в терминах той же концепции, параметризованной для выражений, скрытых в типах. Но обычно невозможно пойти другим путем.
ограничение на swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))
как ожидается, будет достаточно важным сценарием; Вдобавок ко мне это приходит в бизнес, такой как:
template<typename Val, typename It>
void client_code(Val val, It it)
requires Swappable<Val&, decltype(*it)>
// ^^^^^^^^^^^^^--.
// |
// hiding an expression into a type! ------`
{
ranges::swap(val, *it);
}
согласованность: по большей части другие концепции TS следуют тому же соглашению и параметризуются по типам выражений
Но почему по большей части?
Потому что существует третий тип концептуального параметра: тип, обозначающий … тип. Хороший пример тому DerivedFrom<Derived, Base>()
какое значение не дает вам допустимых выражений (или способов использования переменных) в обычном смысле.
Фактически, например, Constructible<Arg, Inits...>()
первый аргумент Arg
можно интерпретировать двумя способами:
Arg
обозначает тип, то есть принимает конструктивность как неотъемлемое свойство типаArg
является объявленным типом создаваемой переменной, то есть ограничение подразумевает, что Arg imaginary_var { std::declval<Inits>()... };
является действительнымЗавершу свое личное замечание: я думаю, что читатель не должен (пока) делать вывод, что он должен писать свои собственные концепции одинаково только потому, что концепции над выражениями представляются, по крайней мере, с точки зрения автора концептов, как надмножество понятий над переменными.
Существуют и другие факторы, и моя проблема заключается именно в удобстве использования с точки зрения концептуального клиента. и все эти детали я тоже упомянул только мимоходом. Но это не имеет отношения к вопросу, и этот ответ уже достаточно длинный, поэтому я оставлю эту историю на другой раз.
Я все еще очень плохо знаком с концепциями, поэтому не стесняйтесь указывать на любые ошибки, которые мне нужно исправить в этом ответе. Ответ состоит из трех разделов: Первый непосредственно касается использования std::forward
вторая расширяется Swappable
и третий касается внутренней ошибки.
Это похоже на опечатку1, и скорее всего должно быть requires(T&& t, U&& u)
, В этом случае совершенная пересылка используется для обеспечения правильной оценки концепции для ссылок lvalue и rvalue, гарантируя, что только ссылки lvalue будут помечены как заменяемые.
Полный Диапазоны TS Swappable
концепция, на котором это основано, полностью определяется как:
template <class T>
concept bool Swappable() {
return requires(T&& a, T&& b) {
ranges::swap(std::forward<T>(a), std::forward<T>(b));
};
}
template <class T, class U>
concept bool Swappable() {
return ranges::Swappable<T>() &&
ranges::Swappable<U>() &&
ranges::CommonReference<const T&, const U&>() &&
requires(T&& t, U&& u) {
ranges::swap(std::forward<T>(t), std::forward<U>(u));
ranges::swap(std::forward<U>(u), std::forward<T>(t));
};
}
Концепция, показанная на Ограничения и понятия страница является упрощенной версией этого, которая, как представляется, предназначена для минимальной реализации концепции библиотеки Swappable
, Как полное определение указывает requires(T&&, U&&)
, само собой разумеется, что эта упрощенная версия должна также. std::forward
Таким образом, используется с ожиданием того, что t
а также u
пересылаем ссылки.
1: Комментарий Кубби, Сделано в то время как я тестировал код, проводил исследования и ел ужин, подтверждает, что это опечатка.
Swappable
, Не стесняйтесь, если это вас не касается.]
Обратите внимание, что этот раздел применяется только в том случае, если Swappable
определяется вне пространства имен std
; если определено в std
как представляется в проект, два std::swap()
s будет автоматически учитываться при разрешении перегрузки, что означает, что для их включения не требуется никакой дополнительной работы. Спасибо, Cubbi за ссылку на проект и заявив, что Swappable
был взят прямо из него.
Обратите внимание, однако, что упрощенная форма сама по себе не является полной реализацией Swappable
если using std::swap
уже было указано. [swappable.requirements/3]
утверждает, что разрешение перегрузки должно учитывать как два std::swap()
шаблоны и любые swap()
найдены ADL (то есть, разрешение должно происходить так, как будто декларация об использовании using std::swap
было указано). Поскольку концепции не могут содержать декларации использования, более Swappable
может выглядеть примерно так:
template<typename T, typename U = T>
concept bool ADLSwappable = requires(T&& t, U&& u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
template<typename T, typename U = T>
concept bool StdSwappable = requires(T&& t, U&& u) {
std::swap(std::forward<T>(t), std::forward<U>(u));
std::swap(std::forward<U>(u), std::forward<T>(t));
};
template<typename T, typename U = T>
concept bool Swappable = ADLSwappable<T, U> || StdSwappable<T, U>;
Это расширилось Swappable
позволит правильно определить параметры, которые соответствуют концепции библиотеки, вот так.
Swappable
сам. Не стесняйтесь, если это вас не касается.]
Чтобы использовать это, однако, f()
нужно несколько модификаций. Скорее, чем:
void f(Swappable& x) {}
Вместо этого следует использовать одно из следующих:
template<typename T>
void f(T&& x) requires Swappable<T&&> {}
template<typename T>
void f(T& x) requires Swappable<T&> {}
Это связано с взаимодействием между GCC и правилами разрешения концепций и, вероятно, будет рассмотрено в будущих версиях компилятора. Использование выражения ограничения обходит взаимодействие, которое, как я считаю, отвечает за внутреннюю ошибку, что делает его жизнеспособным (если более подробным) показателем временной задержки.
Кажется, внутренняя ошибка вызвана тем, как GCC обрабатывает правила разрешения концепций. Когда он встречает эту функцию:
void f(Swappable& x) {}
Поскольку концепты функций могут быть перегружены, разрешение концептов выполняется, когда имена концептов встречаются в определенных контекстах (например, при использовании в качестве спецификатора ограниченного типа, например, Swappable
это здесь). Таким образом, GCC пытается решить Swappable
в соответствии с правилом разрешения концепции № 1, в Концепция разрешения раздел этой страницы:
Swappable
используется без списка параметров, в качестве аргумента используется один подстановочный знак. Этот подстановочный знак может соответствовать любому возможному параметру шаблона (будь то тип, тип или шаблон) и, таким образом, идеально подходит для T
,Как Swappable
второй параметр не соответствует аргументу, будет использоваться его аргумент шаблона по умолчанию, как указано после пронумерованных правил; Я считаю, что это проблема. Как T
Сейчас (wildcard)
упрощенным подходом было бы временно создать экземпляр U
в качестве другого подстановочного знака или копии первого подстановочного знака, и определить, Swappable<(wildcard), (wildcard)>
соответствует шаблону template<typename T, typename U>
(оно делает); тогда можно было бы вывести T
и используйте это, чтобы правильно определить, разрешает ли он Swappable
концепция.
Вместо этого GCC, кажется, достиг Catch-22: он не может быть создан U
пока это не выводит T
, но это не может вывести T
пока он не определит, является ли это Swappable
правильно разрешается до Swappable
концепция … без которой не обойтись U
, Итак, нужно выяснить, что U
прежде чем он сможет выяснить, имеем ли мы право Swappable
, но нужно знать, имеем ли мы право Swappable
прежде чем он сможет выяснить, что U
является; столкнувшись с этой неразрешимой загадкой, он имеет аневризму, опрокидывается и умирает.