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


161

Один из углов концепций C ++ 20 заключается в том, что есть определенные ситуации, в которых вы должны писать requires requires. Например, этот пример из [expr.prim.req] / 3 :

Требуется выражение также может быть использовано в требуете-положение ([Темп]) в качестве способа написания специальных ограничений на аргументах шаблона , такие как один ниже:

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)


25
Предложение: «Есть ли что-то, что требует, требует, требует?». Если серьезно, у меня есть догадка, что это та же самая причина noexcept(noexcept(...)).
Квентин

11
По requiresмоему мнению, это два омонима: они выглядят одинаково, пишутся одинаково, пахнут одинаково, но по своей сути различны. Если бы я предложил исправление, я бы предложил переименовать один из них.
СМУ

5
@YSC - co_requires? (Извините, не удержался).
StoryTeller - Unslander Моника

122
Где остановится безумие? Следующее, что вы знаете, у нас будет long long.
Eljay

8
@StoryTeller: «Требуется требуется требуется?» было бы еще более аллитеративным !!
PW

Ответы:


81

Это потому, что грамматика требует этого. Оно делает.

requiresОграничение не должно использовать requiresвыражение. Он может использовать любое более или менее произвольное логическое константное выражение. Следовательно, requires (foo)должно быть законное requiresограничение.

requires Выражение (та вещь , что тесты некоторые вещи следуют ли определенные ограничения) является идеальной отправной точкой конструкт; это просто введено тем же ключевым словом. requires (foo f)будет началом правильного requiresвыражения.

То, что вы хотите, это то, что если вы используете requiresв месте, которое принимает ограничения, вы должны быть в состоянии сделать «ограничение + выражение» из requiresпредложения.

Итак, вот вопрос: если вы поместите requires (foo)в место, подходящее для ограничения требует ... как далеко должен пройти анализатор, чтобы понять, что это ограничение требуется а не выражение constraint + так, как вы этого хотите быть?

Учти это:

void bar() requires (foo)
{
  //stuff
}

Если fooэто тип, то (foo)это список параметров выражения требует, и все в{} является не телом функции, а телом этого requiresвыражения. В противном случае, fooявляется выражением в requiresпредложении.

Ну, вы могли бы сказать, что компилятор должен просто выяснить, что fooявляется первым. Но C ++ действительно не нравится, когда базовый акт парсинга последовательности токенов требует, чтобы компилятор выяснил, что означают эти идентификаторы, прежде чем он сможет понять токены. Да, C ++ является контекстно-зависимым, так что это происходит. Но комитет предпочитает избегать этого там, где это возможно.

Так что да, это грамматика.


2
Имеет ли смысл иметь список параметров с типом, но без имени?
Натан Оливер

3
@Quentin: Конечно, в грамматике C ++ есть случаи зависимости от контекста. Но комитет действительно пытается минимизировать это, и они определенно не любят добавлять больше .
Николь Болас

3
@RobertAndrzejuk: Если requiresпоявляется после <>набора аргументов шаблона или после списка параметров функции, это предложение require. Если requiresпоявляется, где выражение является действительным, то это выражение require. Это может быть определено структурой дерева разбора, а не содержимым дерева разбора (специфика определения идентификатора будет содержимым дерева).
Николь Болас

6
@RobertAndrzejuk: Конечно, выражение require могло бы использовать другое ключевое слово. Но ключевые слова в C ++ имеют огромные затраты, поскольку они могут сломать любую программу, которая использовала идентификатор, который стал ключевым словом. В предложении о концепции уже введены два ключевых слова: conceptи requires. Представление третьего, когда второе сможет охватить оба случая без грамматических проблем и небольшого количества проблем, с которыми сталкиваются пользователи, является просто расточительным. В конце концов, единственная визуальная проблема заключается в том, что ключевое слово повторяется дважды.
Николь Болас

3
@RobertAndrzejuk В любом случае это плохая практика - вставлять подобные ограничения, так как вы не получаете подчинение, как если бы вы написали концепцию. Поэтому использование идентификатора для такой малозаметной, не рекомендуемой функции не является хорошей идеей.
Rakete1111

60

Ситуация в точности аналогична noexcept(noexcept(...)). Конечно, это больше похоже на плохую вещь, чем на хорошую, но позвольте мне объяснить. :) Начнем с того, что вы уже знаете:

В C ++ 11 есть « noexcept-clauses» и «-expressions» noexcept. Они делают разные вещи.

  • В noexcept-классе говорится: «Эта функция не должна быть исключена, когда ... (некоторое условие)». Он идет по объявлению функции, принимает логический параметр и вызывает поведенческое изменение в объявленной функции.

  • noexcept-Expression говорит, «Compiler, скажите , пожалуйста , является ли (некоторое выражение) является noexcept.» Это само логическое выражение. У него нет «побочных эффектов» на поведение программы - он просто запрашивает у компилятора ответ на вопрос «да / нет». "Это выражение не исключение?"

Мы можем гнездятся в noexcept-expression внутри noexcept-clause, но мы , как правило , считают это плохой стиль , чтобы сделать это.

template<class T>
void incr(T t) noexcept(noexcept(++t));  // NOT SO HOT

Считается лучшим стилем инкапсулировать noexceptвыражение в признаке типа.

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

Рабочий проект C ++ 2a имеет « requires-clauses» и «-expressions» requires. Они делают разные вещи.

  • В requires-классе говорится: «Эта функция должна участвовать в разрешении перегрузки, когда ... (некоторое условие)». Он идет по объявлению функции, принимает логический параметр и вызывает поведенческое изменение в объявленной функции.

  • requires-Expression говорит, «Compiler, скажите , пожалуйста , является ли это (некоторый набор выражений) хорошо сформированы.» Это само логическое выражение. У него нет «побочных эффектов» на поведение программы - он просто запрашивает у компилятора ответ на вопрос «да / нет». "Это выражение правильно сформировано?"

Мы можем гнездятся в requires-expression внутри requires-clause, но мы , как правило , считают это плохой стиль , чтобы сделать это.

template<class T>
void incr(T t) requires (requires(T t) { ++t; });  // NOT SO HOT

Считается лучшим стилем инкапсулировать requiresвыражение в признаке типа ...

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

... или в концепции (рабочий проект C ++ 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

1
Я действительно не покупаю этот аргумент. noexceptесть проблема , что noexcept(f())может означать либо интерпретировать f()как логические , который мы используем , чтобы установить спецификации или проверить , является ли или не f()является noexcept. requiresне имеет этой двусмысленности, потому что выражения, проверяющие правильность, уже должны быть введены с {}s. После этого аргумент в основном звучит так: «Грамматика так говорит».
Барри

@ Барри: Смотрите этот комментарий . Похоже, {}являются необязательными.
Эрик

1
@Eric Не {}являются обязательными, это не то, что показывает этот комментарий. Тем не менее, это отличный комментарий, демонстрирующий разбор неоднозначности. Возможно, принял бы этот комментарий (с некоторыми пояснениями) как самостоятельный ответ
Барри

1
requires is_nothrow_incrable_v<T>;должно бытьrequires is_incrable_v<T>;
Руслан

16

Я думаю, что страница концепций 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; };и замените ее Addable<T>, и вы получите:

template<typename T> requires requires (T x) { x + x; }
T add(T a, T b) { return a + b; }

о чем ты спрашиваешь.


4
Я не думаю, что это вопрос, который задают. Это объясняет грамматику, более или менее.
Прохожий

Но почему вы не можете иметь template<typename T> requires (T x) { x + x; }и требовать, чтобы требование могло быть и предложением, и выражением?
Натан Оливер

2
@NathanOliver: потому что вы заставляете компилятор интерпретировать одну конструкцию как другую. Предложение requires-as-ограничений не обязательно должно быть requiresвыражением. Это всего лишь одно из возможных применений.
Никол Болас

2
@TheQuantumPhysicist То, что я получил с моим комментарием, - этот ответ только объясняет синтаксис. Не то, что фактическая техническая причина у нас есть requires requires. Они могли бы добавить что-то в грамматику, template<typename T> requires (T x) { x + x; }но не сделали этого. Барри хочет знать, почему они этого не сделали
NathanOliver

3
Если мы действительно играем в «найти грамматику-двусмысленность» здесь, хорошо, я укушу. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; означает что-то другое, если вы удалите один из requireses.
Quuxplusone

12

Я нашел комментарий от Эндрю Саттона (одного из авторов концепции, который реализовал его в gcc) весьма полезным в этом отношении, поэтому я подумал, что просто процитирую его здесь почти полностью:

Не так давно выражения-требования (фраза, введенная вторым требованием) не было разрешено в выражениях-ограничениях (фраза, введенная первым, требует). Это может появиться только в определениях понятий. На самом деле, это именно то, что предлагается в разделе этого документа, где появляется эта претензия.

Однако в 2016 году было предложено ослабить это ограничение [Примечание редактора: P0266 ]. Обратите внимание на зачеркнутый пункт 4 в разделе 4 документа. И таким образом родился, требует требует.

Честно говоря, я никогда не применял это ограничение в GCC, поэтому это всегда было возможно. Я думаю, что Уолтер, возможно, обнаружил это и нашел это полезным, что привело к этой статье.

Чтобы никто не думал, что я не чувствителен к письму, требуется дважды, я потратил некоторое время, пытаясь определить, можно ли это упростить. Краткий ответ: нет.

Проблема состоит в том, что есть две грамматические конструкции, которые необходимо ввести после списка параметров шаблона: очень часто выражение ограничения (например P && Q) и иногда синтаксические требования (например requires (T a) { ... }). Это называется требованием выражения.

Первое требует вводит ограничение. Второе требование вводит выражение-требование. Это просто способ грамматики. Я не нахожу это смущающим вообще.

В какой-то момент я пытался свести их к одному требованию. К сожалению, это приводит к серьезным проблемам с анализом. Вы не можете легко сказать, например, (означает ли a после запроса вложенное подвыражение или список параметров. Я не верю, что существует идеальное устранение неоднозначности этих синтаксисов (см. Обоснование унифицированного синтаксиса инициализации; эта проблема также существует).

Таким образом, вы делаете выбор: make требует ввести выражение (как оно делает сейчас) или заставить его ввести параметризованный список требований.

Я выбрал текущий подход, потому что большую часть времени (как почти в 100% случаев) я хочу что-то иное, чем выражение-требование. И в чрезвычайно редком случае я хотел получить выражение для специальных ограничений, я действительно не против написать слово дважды. Это очевидный показатель того, что я не разработал достаточно надежную абстракцию для шаблона. (Потому что, если бы я имел, у него было бы имя.)

Я мог бы сделать так, чтобы требования требовали введения выражения требований. Это на самом деле хуже, потому что практически все ваши ограничения начнут выглядеть так:

template<typename T>
  requires { requires Eq<T>; }
void f(T a, T b);

Здесь второе требование называется вложенным требованием; он оценивает свое выражение (другой код в блоке выражения require не оценивается). Я думаю, что это намного хуже, чем статус-кво. Теперь вы можете писать требует дважды везде.

Я мог бы также использовать больше ключевых слов. Это проблема сама по себе - и это не просто потеря велосипеда. Возможно, есть способ «перераспределить» ключевые слова, чтобы избежать дублирования, но я не задумывался об этом серьезно. Но это не меняет сути проблемы.


-10

Потому что вы говорите, что у вещи A есть требование B, а у требования B есть требование C.

Вещь A требует B, который в свою очередь требует C.

Предложение «требует» само по себе требует чего-то.

У вас есть вещь A (требующая B (требующая C)).

Мех. :)


4
Но согласно другим ответам, первое и второе requiresконцептуально не одно и то же (один - это предложение, другой - выражение). На самом деле, если я правильно понимаю, два набора ()in requires (requires (T x) { x + x; })имеют очень разные значения (внешнее является необязательным и всегда содержит логический constexpr; внутреннее является обязательной частью введения выражения require и не допускает фактических выражений).
Макс

2
@MaxLanghof Вы говорите, что требования отличаются? : D
Гонки
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.