Зачем использовать 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; столкнувшись с этой неразрешимой головоломкой, у нее аневризма, кили и умирает.