Почему мы требуем требует требует?

Один из углов концепций С++ 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)).

Мех. :)