Почему мы требуем требует требует?
Один из углов концепций С++ 20 заключается в том, что существуют определенные ситуации, в которых вам нужно написать " requires requires
. Например, этот пример из [expr.prim.req]/3:
Выражение require также может использоваться в предложении require ([temp]) как способ написания специальных ограничений для аргументов шаблона, таких как приведенный ниже:
template<typename T>
requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
Первый требует вводит предложение require, а второй вводит выражение require.
Какова техническая причина, по которой нужно второе ключевое слово requires
? Почему мы не можем просто разрешить писать:
template<typename T>
requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
(Примечание: пожалуйста, не отвечайте, что грамматика requires
этого)
Ответы
Ответ 1
Это потому, что грамматика требует этого. Оно делает.
Ограничение requires
не использовать выражение requires
. Он может использовать любое более или менее произвольное логическое константное выражение. Следовательно, requires (foo)
должен быть законным ограничением requires
.
requires
выражение (то, что проверяет, соответствуют ли определенные вещи определенным ограничениям) - это отдельная конструкция; это просто введено тем же ключевым словом. requires (foo f)
будет начало действительного requires
выражений.
То, что вы хотите, что если вы используете requires
в месте, которое принимает ограничения, вы должны быть в состоянии сделать "ограничение + выражение" из requires
оговорки.
Итак, вот вопрос: если вы помещаете requires (foo)
в место, которое подходит для ограничения require... насколько далеко должен пройти анализатор, прежде чем он сможет понять, что это ограничение require, а не ограничение + выражение как вы хотите, чтобы это было?
Учти это:
void bar() requires (foo)
{
//stuff
}
Если foo
является типом, то (foo)
является списком параметров выражения require, и все в {}
является не телом функции, а телом, для которого requires
выражение. В противном случае, foo
это выражение в requires
оговорки.
Ну, вы могли бы сказать, что компилятор должен просто выяснить, что foo
является первым. Но C++ действительно не нравится, когда базовый акт парсинга последовательности токенов требует, чтобы компилятор выяснил, что означают эти идентификаторы, прежде чем он сможет понять токены. Да, C++ является контекстно-зависимым, так что это происходит. Но комитет предпочитает избегать этого там, где это возможно.
Так что да, это грамматика.
Ответ 2
Ситуация в точности аналогична noexcept(noexcept(...))
. Конечно, это больше похоже на плохую вещь, чем на хорошую, но позвольте мне объяснить. :) Начнем с того, что вы уже знаете:
C++ 11 имеет " noexcept
-clauses" и " noexcept
-expressions." Они делают разные вещи.
-
noexcept
-clause говорит: "Эта функция должна быть noexcept, когда... (некоторое условие)." Он идет по объявлению функции, принимает логический параметр и вызывает поведенческое изменение в объявленной функции.
-
noexcept
-expression говорит, "Compiler, скажите, пожалуйста, является ли (некоторое выражение) является noexcept." Это само по себе логическое выражение. У него нет "побочных эффектов" на поведение программы - он просто запрашивает у компилятора ответ на вопрос "да/нет". "Это выражение не исключение?"
Мы можем вкладываете noexcept
-expression внутри noexcept
-clause, но мы обычно считаем плохой стиль, чтобы сделать это.
template<class T>
void incr(T t) noexcept(noexcept(++t)); // NOT SO HOT
Это считается лучшим стилем для инкапсулирования noexcept
-expression в машинке признаке.
template<class T> inline constexpr bool is_nothrow_incrable_v =
noexcept(++std::declval<T&>()); // BETTER, PART 1
template<class T>
void incr(T t) noexcept(is_nothrow_incrable_v<T>); // BETTER, PART 2
Рабочий проект С++ 2a имеет " requires
-clauses" и " requires
-expressions". Они делают разные вещи.
-
A requires
-clause говорит: "Эта функция должна участвовать в разрешении перегрузки, когда... (некоторое условие)". Он идет по объявлению функции, принимает логический параметр и вызывает поведенческое изменение в объявленной функции.
-
A requires
-expression: "Компилятор, пожалуйста, скажите мне, правильно ли (некоторый набор выражений) сформирован". Это само по себе логическое выражение. У него нет "побочных эффектов" на поведение программы - он просто запрашивает у компилятора ответ на вопрос "да/нет". "Это выражение правильно сформировано?"
Мы можем вкладывать requires
-expression в requires
-clause, но мы обычно считаем это плохим стилем.
template<class T>
void incr(T t) requires (requires(T t) { ++t; }); // NOT SO HOT
Это считается лучшим стилем для инкапсулирования requires
-expression в машинке черте...
template<class T> inline constexpr bool is_incrable_v =
requires(T t) { ++t; }; // BETTER, PART 1
template<class T>
void incr(T t) requires is_incrable_v<T>; // BETTER, PART 2
... или в концепции (рабочий проект С++ 2a).
template<class T> concept Incrable =
requires(T t) { ++t; }; // BETTER, PART 1
template<class T>
void incr(T t) requires Incrable<T>; // BETTER, PART 2
Ответ 3
Я думаю, что страница концепций cppreference объясняет это. Я могу объяснить с помощью "математики", почему это должно быть так:
Если вы хотите определить концепцию, вы делаете это:
template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression
Если вы хотите объявить функцию, которая использует эту концепцию, вы делаете это:
template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }
Теперь, если вы не хотите определять концепцию отдельно, я полагаю, все, что вам нужно сделать, это какая-то замена. Взять эту часть requires (T x) { x + x; };
requires (T x) { x + x; };
и замените часть Addable<T>
, и вы получите:
template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }
о чем ты спрашиваешь.
Ответ 4
Я нашел комментарий Эндрю Саттона (одного из авторов концепции, который реализовал его в gcc) весьма полезным в этом отношении, поэтому я подумал, что просто процитирую его здесь почти полностью:
Не так давно выражения-требования (фраза, введенная вторым требованием) не было разрешено в выражениях-ограничениях (фраза, введенная первым, требует). Это может появиться только в определениях понятий. На самом деле, это именно то, что предлагается в разделе этого документа, где появляется эта претензия.
Однако в 2016 году было предложено ослабить это ограничение [Примечание редактора: P0266 ]. Обратите внимание на зачеркнутый пункт 4 в разделе 4 документа. И таким образом родился, требует требует.
Честно говоря, я никогда не применял это ограничение в GCC, поэтому это всегда было возможно. Я думаю, что Уолтер, возможно, обнаружил это и нашел это полезным, что привело к этой статье.
Чтобы никто не думал, что я не чувствителен к письму, требуется дважды, я потратил некоторое время, пытаясь определить, можно ли это упростить. Краткий ответ: нет.
Проблема заключается в том, что есть две грамматические конструкции, которые необходимо ввести после списка параметров шаблона: очень часто это выражение ограничения (например, P && Q
) и иногда синтаксические требования (например, requires (T a) {... }
). Это называется требованием выражения.
Первое требует вводит ограничение. Второе требование вводит выражение-требование. Именно так и сочиняется грамматика. Я не нахожу это смущающим вообще.
В какой-то момент я пытался свести их к одному требованию. К сожалению, это приводит к серьезным проблемам с анализом. Вы не можете легко сказать, например, если a (
после require обозначает вложенное подвыражение или список параметров. Я не верю, что существует идеальная неоднозначность этих синтаксисов (см. Обоснование для единообразного синтаксиса инициализации; эта проблема там тоже).
Таким образом, вы делаете выбор: make требует ввести выражение (как оно делает сейчас) или заставить его ввести параметризованный список требований.
Я выбрал текущий подход, потому что большую часть времени (как почти в 100% случаев) я хочу что-то иное, чем выражение-требование. И в чрезвычайно редком случае я хотел получить выражение для специальных ограничений, я действительно не против написать слово дважды. Это очевидный показатель того, что я не разработал достаточно надежную абстракцию для шаблона. (Потому что, если бы я имел, у него было бы имя.)
Я мог бы сделать так, чтобы требования требовали введения выражения требований. Это на самом деле хуже, потому что практически все ваши ограничения начнут выглядеть так:
template<typename T>
requires { requires Eq<T>; }
void f(T a, T b);
Здесь второе требование называется вложенным требованием; он оценивает свое выражение (другой код в блоке выражения require не оценивается). Я думаю, что это намного хуже, чем статус-кво. Теперь вы можете писать требует дважды везде.
Я мог бы также использовать больше ключевых слов. Это проблема сама по себе - и это не просто потеря велосипеда. Возможно, есть способ "перераспределить" ключевые слова, чтобы избежать дублирования, но я не задумывался об этом серьезно. Но это не меняет сути проблемы.
Ответ 5
Потому что вы говорите, что у вещи A есть требование B, а у требования B есть требование C.
Вещь A требует B, который в свою очередь требует C.
Предложение "требует" само по себе требует чего-то.
У вас есть вещь A (требующая B (требующая C)).
Мех. :)