Зачем использовать std :: forward в концепциях?

Я читал Страница 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 неправильно? Я что-то пропустил?

22

Решение

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

Coliru демонстрация

А что насчет второго бита, в котором упоминаются 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 демонстрация

То, что появляется в записи 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>()... }; является действительным

Завершу свое личное замечание: я думаю, что читатель не должен (пока) делать вывод, что он должен писать свои собственные концепции одинаково только потому, что концепции над выражениями представляются, по крайней мере, с точки зрения автора концептов, как надмножество понятий над переменными.

Существуют и другие факторы, и моя проблема заключается именно в удобстве использования с точки зрения концептуального клиента. и все эти детали я тоже упомянул только мимоходом. Но это не имеет отношения к вопросу, и этот ответ уже достаточно длинный, поэтому я оставлю эту историю на другой раз.

5

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

Я все еще очень плохо знаком с концепциями, поэтому не стесняйтесь указывать на любые ошибки, которые мне нужно исправить в этом ответе. Ответ состоит из трех разделов: Первый непосредственно касается использования 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 позволит правильно определить параметры, которые соответствуют концепции библиотеки, вот так.


[Следующее относится к внутренней ошибке 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, в Концепция разрешения раздел этой страницы:

  1. Как Swappable используется без списка параметров, в качестве аргумента используется один подстановочный знак. Этот подстановочный знак может соответствовать любому возможному параметру шаблона (будь то тип, тип или шаблон) и, таким образом, идеально подходит для T,
  2. Как 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 является; столкнувшись с этой неразрешимой загадкой, он имеет аневризму, опрокидывается и умирает.

4