Зачем использовать std:: forward в понятиях?
Я читал страницу cppreference на Constraints и заметил этот пример:
// 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
с lvalues и не будут ли выражения forward
быть rvalues, когда 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
неправильной? Я что-то пропустил?
Ответы
Ответ 1
Мы не хотим вызывать swap с lvalues [...]
Это очень хороший вопрос. Вопрос конкретно о дизайне API: какой смысл или значение должен дать разработчик концептуальной библиотеки для параметров его понятий?
Быстрое описание требований к замене. То есть фактические требования, которые уже появляются в сегодняшнем стандарте и были здесь с тех пор, пока понятия-lite:
- Объект
t
заменяется с объектом u
тогда и только тогда, когда: - [...] выражения
swap(t, u)
и swap(u, t)
действительны [...]
[...]
Значение rvalue или lvalue t
можно заменять тогда и только тогда, когда t заменяется с любым значением r или значением l соответственно типа 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&&> );
(Теперь технически Стандарт говорил в терминах объектов, ссылки на которые не являются. Так как ссылки относятся не только к объектам или functions а предназначены для прозрачной поддержки для них, обеспечил очень желательную особенность. Практически мы сейчас работаем с точки зрения переменных, а не только объектов.)
Здесь очень важная связь: int&&
- это объявленный тип наших переменных, а также фактический аргумент, переданный концепции, который, в свою очередь, снова становится объявленным типом наших lhs
и rhs
требует параметров. Помните об этом, когда мы копаем глубже.
Coliru demo
Выражение
Теперь как насчет второго бита, который упоминает 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)
.
Coliru demo
Совмещение
То, что появляется в записи cppreference, представляет собой резюме концепции Swappable
, заданной TS диапазона. Если бы я мог догадаться, я бы сказал, что Ranges 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>()... };
действительно
Как мне написать свои собственные понятия?
Я завершу личную заметку: я думаю, что читатель не должен заканчивать (тем не менее), что они должны писать свои собственные концепции одинаково, просто потому, что концепции над выражениями появляются, по крайней мере, с точки зрения автора концепции, как надмножество понятий над переменными.
В игре есть и другие факторы, и моя забота заключается именно в удобстве использования с точки зрения концептуального клиента и все эти детали, о которых я только упомянул, тоже. Но это действительно не связано с вопросом, и этот ответ уже достаточно длинный, поэтому я оставляю эту историю в другое время.
Ответ 2
Я все еще очень новичок в концепциях, поэтому не стесняйтесь указывать на любые ошибки, которые мне нужно исправить в этом ответе. Ответ разделен на три раздела: первый непосредственно рассматривает использование std::forward
, второй расширяется на Swappable
, а третий касается внутренней ошибки.
Кажется, что это опечатка 1 и, вероятно, должна быть requires(T&& t, U&& u)
. В этом случае идеальная пересылка используется для обеспечения правильной оценки концепции как для ссылок lvalue, так и для rvalue, гарантируя, что только ссылки lvalue будут отмечены как сменные.
Полный Ranges TS Swappable
concept, на котором он основан, полностью определяется как:
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: комментарий Cubbi, сделанный во время тестирования кода, проведения исследований и ужина, подтверждает, что это опечатка.
[Следующее расширяется на Swappable
. Не стесняйтесь пропустить его, если это вас не касается.]
Обратите внимание, что этот раздел применяется только в том случае, если Swappable
определено вне пространства имен std
; если он определен в std
, поскольку он, как представляется, находится в черновик, два std::swap()
будут автоматически учитываться при разрешении перегрузки, что означает никакая дополнительная работа не требуется для их включения. Спасибо, 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
позволит правильно определить параметры, которые соответствуют концепции библиотеки, как это.
[Следующее относится к внутренней ошибке GCC и непосредственно не относится к 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
; столкнувшись с этой неразрешимой головоломкой, у нее аневризма, кили и умирает.