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

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

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

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

Первый требует вводит требует-предложение, а второй вводит требует-выражение.

Какова техническая причина необходимости во втором requires ключевом слове? Почему мы не можем просто разрешить писать:

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

(Примечание: пожалуйста, не отвечайте, что грамматика requires это)


person Barry    schedule 15.01.2019    source источник
comment
Кажется, requires можно использовать для метода класса шаблона, и это будет неоднозначно template <typename T> struct S {void f(T t) requires requires (T x) {x + x;} { t + t;} };   -  person Jarod42    schedule 15.01.2019
comment
Предложение: Есть ли что-то, что требует, требует, требует ?. Если серьезно, у меня есть подозрение, что это та же причина, по которой стоит noexcept(noexcept(...)).   -  person Quentin    schedule 15.01.2019
comment
Они говорят, что Первый требует вводит предложение-требование, а второй вводит выражение-требование., но это несовместимо с грамматикой, которую они приводят чуть выше   -  person bruno    schedule 15.01.2019
comment
@Quentin С noexcept есть двусмысленность. noexcept(f()) может означать noexcept, если f() оценивается как истина или если f() равно noexcept.   -  person Passer By    schedule 15.01.2019
comment
@PasserBy - Тогда разве это не та же причина? Предложение requires может иметь любой предикат constexpr, не так ли?   -  person StoryTeller - Unslander Monica    schedule 15.01.2019
comment
Одна из причин, по которой я это делаю, - это облегчить синтаксический анализ. Когда компилятор видит requires, он знает, что следующий символ является требованием, поэтому нам нужен дополнительный requires, чтобы сообщить ему, что ему нужно проанализировать это требование как специальное.   -  person NathanOliver    schedule 15.01.2019
comment
На мой взгляд, эти два requires являются омонимами: они выглядят одинаково, пишутся одинаково, пахнут одинаково, но по сути различны. Если бы я предложил решение, я бы предложил переименовать одно из них.   -  person YSC    schedule 15.01.2019
comment
@StoryTeller По-видимому, да, как продемонстрировал Никол Болас, хотя и в меньшей степени.   -  person Passer By    schedule 15.01.2019
comment
Как ни странно, это имеет смысл грамматически. add<T>() требует T, который, в свою очередь, требует добавления возможности к другим T. Так что, хотя это выглядит повторяющимся, это имеет смысл, если вы достаточно об этом думаете. (Где я должен быть добавляемым - это выражение, а функции нужно что-то, что должно быть добавлено, - это ограничение.)   -  person Justin Time - Reinstate Monica    schedule 15.01.2019


Ответы (5)


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

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

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

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

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

Учти это:

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

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

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

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

person Nicol Bolas    schedule 15.01.2019
comment
Имеет ли смысл иметь список параметров с типом, но без имени? - person NathanOliver; 15.01.2019
comment
Это напоминает мне ответ, который я написал где, насколько я могу судить, компилятор не может понять код до определения того, что такое x. Другой пример - ADL, работающий с шаблонами функций только если в области видимости есть аналогичный шаблон функции (demo), иначе синтаксический анализ завершится неудачно. У меня сложилось впечатление, что сильная зависимость от контекста довольно распространена в C ++. - person Quentin; 15.01.2019
comment
@Quentin: В грамматике C ++, безусловно, есть случаи зависимости от контекста. Но комитет действительно пытается свести это к минимуму, и им определенно не нравится добавлять больше. - person Nicol Bolas; 15.01.2019
comment
Если причиной является контекст, то использование одного и того же слова в двух контекстах противоречит намерению. - person Robert Andrzejuk; 15.01.2019
comment
@RobertAndrzejuk: Использование if vs. if constexpr не зависит от контекста, потому что отличить одно от другого легко на основе последовательности токенов. Для последовательности токенов идентификатор - это просто идентификатор. Если природа этого идентификатора определяет, как анализировать последовательность токенов, значит, вы имеете дело с чем-то, что проблематично в анализаторе. Не невозможно, просто зависит от контекста. Использование такого ключевого слова не зависит от контекста в этом отношении, потому что место, где вы используете эти ключевые слова, определяет контекст. - person Nicol Bolas; 15.01.2019
comment
@RobertAndrzejuk: Если requires появляется после <> набора аргументов шаблона или после списка параметров функции, то это предложение требует. Если requires появляется там, где выражение является допустимым, то это выражение требует. Это может быть определено структурой дерева синтаксического анализа, а не содержимым дерева синтаксического анализа (особенности определения идентификатора будут зависеть от содержимого дерева). - person Nicol Bolas; 15.01.2019
comment
Я имею в виду, что тогда это может быть, например, требует ограничения. - person Robert Andrzejuk; 15.01.2019
comment
@RobertAndrzejuk: Конечно, в выражении requires могло быть другое ключевое слово. Но ключевые слова имеют огромные затраты в C ++, так как они могут сломать любую программу, которая использовала идентификатор, который стал ключевым словом. В предложении по концепциям уже представлены два ключевых слова: concept и requires. Введение третьего, когда второй сможет охватить оба случая без грамматических проблем и с несколькими проблемами, с которыми сталкивается пользователь, просто расточительно. В конце концов, единственная визуальная проблема заключается в том, что ключевое слово повторяется дважды. - person Nicol Bolas; 15.01.2019
comment
@RobertAndrzejuk - это плохая практика в любом случае встраивать такие ограничения, поскольку вы не получаете подчинение, как если бы вы написали концепцию. Так что использование идентификатора для такой малоиспользуемой не рекомендованной функции - не лучшая идея. - person Rakete1111; 15.01.2019
comment
В общем, вы вообще не можете разобрать C ++, не зная, какие имена являются типами, а какие - шаблонами, поэтому мне не нравится данный конкретный пример. Но двусмысленность найти легко: это очень похоже на самый неприятный разбор. - person T.C.; 16.01.2019
comment
Почему не может быть синтаксиса: requires concept (T x) { x + x; }? Это похоже на то, что должно было быть учтено. - person Lmn; 02.12.2019
comment
@LeoHeinsaar: На самом деле это не решает проблему: concept name = concept(T x) { x + x; } так же повторяется. Проблема в том, что у вас есть 3 структуры, но вы не хотите создавать 3 ключевых слова. - person Nicol Bolas; 02.12.2019
comment
@NicolBolas Я не очень разбираюсь в аспектах, связанных с грамматикой, но, по крайней мере, на первый взгляд, это очень похоже на проблему пространства, которое раньше требовалось грамматике в двойных угловых скобках vector<vector>>, которое было исправлено 'компилятор за компилятором, пока он не был признан исправляемым и стандартизованным. :-) - person Lmn; 03.12.2019
comment
@LeoHeinsaar: Это не проблема парсинга; это проблема с повторяющимися ключевыми словами. concept name = concept так же плох в плане повторения, как и requires requires. Если вы думаете, что одно неверно, значит, ошибается и другой. - person Nicol Bolas; 03.12.2019
comment
@NicolBolas Чтобы избежать повторения ключевых слов и там, возможно, подойдет auto name = concept или пропустив второй concept, как в concept name = (T x) { x + x; } (концептуальные лямбды). - person Lmn; 03.12.2019
comment
@LeoHeinsaar: Но вы должны иметь возможность использовать то, что мы в настоящее время называем requires выражением вне определения концепции, поэтому вам нужен синтаксис, чтобы я мог поместить выражение requires в if constexpr или что-то еще. И вам нужно иметь возможность определить concept, в котором не используется выражение requires, поэтому мы вернулись к тому моменту, когда синтаксический анализатор предполагает, что (T) означает одно или другое. Единственное действительно хорошее решение здесь - третье ключевое слово; в противном случае достаточно повторить одно из других. - person Nicol Bolas; 03.12.2019

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

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

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

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

Мы можем вложить noexcept-выражение в noexcept-предложение, но мы обычно считаем это плохим стилем.

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» и «requires-expression». Они делают разные вещи.

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

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

Мы можем вложить requires-выражение в requires-предложение, но мы обычно считаем это плохим стилем.

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 Working Draft).

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
person Quuxplusone    schedule 15.01.2019
comment
Я действительно не верю этому аргументу. noexcept имеет проблему, что noexcept(f()) может означать либо интерпретировать f() как логическое значение, которое мы используем для установки спецификации , либо проверять, является ли f() noexcept. requires не имеет этой двусмысленности, потому что выражения, проверяющие его действительность, уже должны быть введены с помощью {}s. После этого аргумент, по сути, так гласит грамматика. - person Barry; 16.01.2019
comment
@Barry: см. этот комментарий. Похоже, {} не являются обязательными. - person Eric; 16.01.2019
comment
@Eric {} не являются обязательными, это не то, что показывает этот комментарий. Однако это отличный комментарий, демонстрирующий неоднозначность синтаксического анализа. Вероятно, принял бы этот комментарий (с некоторыми пояснениями) как отдельный ответ - person Barry; 16.01.2019
comment
requires is_nothrow_incrable_v<T>; должно быть requires is_incrable_v<T>; - person Ruslan; 16.01.2019

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

что объясняет механику. Почему лучше всего проиллюстрировать на примере двусмысленности, которая может возникнуть, если мы изменим язык, чтобы принять один requires в качестве сокращения для requires requires.

constexpr int x = 42;

template<class T>
void f(T) requires(T (x)) { (void)x; };

template<class T>
void g(T) requires requires(T (x)) { (void)x; };

int main(){
    g<bool>(0);
}

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

Единственная разница между f и g - это удвоение «требует». И все же семантическая разница между f и g огромна:

  • g - это просто объявление функции, f - полное определение
  • f принимает только bool, g принимает все типы, приводимые к void
  • g затеняет x своим собственным (излишне заключенным в скобки) x, но
  • f приводит глобальный x к заданному типу T

Очевидно, мы бы не хотели, чтобы компилятор автоматически менял одно на другое. Эту проблему можно было бы решить, используя отдельное ключевое слово для двух значений requires, но, когда это возможно, C ++ пытается развиваться, не вводя слишком много новых ключевых слов, поскольку это ломает старые программы.

person The Quantum Physicist    schedule 15.01.2019
comment
Я не думаю, что вопрос задает вопрос. Это более или менее объясняет грамматику. - person Passer By; 15.01.2019
comment
Но почему у вас не может быть template<typename T> requires (T x) { x + x; } и require, чтобы require могло быть как предложением, так и выражением? - person NathanOliver; 15.01.2019
comment
@NathanOliver: Потому что вы заставляете компилятор интерпретировать одну конструкцию как другую. Предложение requires-as-constraint не должно быть requires-выражением. Это всего лишь одно из возможных его применений. - person Nicol Bolas; 15.01.2019
comment
@NathanOliver Я не думаю, что на это есть какой-то волшебный ответ. Это просто неоднозначно, и у компилятора есть определенные ожидания, чтобы он правильно разбирал вещи. Возможно, в следующей версии C ++, если они сочтут это не двусмысленным, они смогут это поддержать. - person The Quantum Physicist; 15.01.2019
comment
@TheQuantumPhysicist То, что я получил в своем комментарии, это ответ, просто объясняющий синтаксис. Не по какой технической причине у нас есть requires requires. Они могли бы добавить что-нибудь в грамматику, чтобы разрешить template<typename T> requires (T x) { x + x; }, но они этого не сделали. Барри хочет знать, почему они этого не сделали - person NathanOliver; 15.01.2019
comment
@NathanOliver Я не думаю, что есть ответ на этот вопрос. Он полностью и полностью складывается с синтаксической точки зрения (как объяснил Никол). Опять же, может быть, в следующей версии C ++ они разрешат это, и пока не ясно, однозначно ли это. Я не уверен, что Барри об этом спрашивает. - person The Quantum Physicist; 15.01.2019
comment
Если мы действительно играем здесь в поиск грамматической двусмысленности, хорошо, я укушу. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; означает нечто иное, если вы удалите один из requireses. - person Quuxplusone; 16.01.2019
comment
Упущенная возможность использовать там параметр template template при демонстрации requires requires - person Mark K Cowan; 16.01.2019
comment
@Quuxplusone, этот пример потрясающий. Не могли бы вы уточнить, в чем разница? Мне кажется, что g - это просто объявление функции, которое требует, чтобы параметр шаблона был преобразован в void (так что все совпадает), в то время как f также является определением, но я не уверен. - person TamaMcGlinn; 16.07.2021
comment
@TamaMcGlinn: Думаю, ты понял. В Godbolt, который я опубликовал выше, f с одним requires является определением: он ограничен (T(x)), т.е. (bool(42)) (поскольку T это bool), т.е. true. Его тело { (void)x; } с лишним завершающим ;. g с requires requires - это объявление, ограниченное requires (T (x)) { (void)x; }, что подходит для всех T (кроме cv-квалифицированного void и , возможно, мерзкие типы); и у него нет тела. - person Quuxplusone; 16.07.2021
comment
Я понимаю; Godbolt, по-видимому, не запускает компоновщик, поэтому это объясняет, почему он, похоже, принимает вызов g<bool>(0); из main, но это действительно не удается во время компоновки, как и ожидалось, когда я пробую его локально. Использование f, конечно, нормально, у него есть определение, которое приводит параметр к void и ничего не делает с ним. - person TamaMcGlinn; 17.07.2021
comment
clang -Weverything помогает; (1) лишние круглые скобки вокруг имени формального параметра в предложении requires для g - которые полезны для f. (2) определение функции f имеет дополнительный завершающий ; - для g это необходимо, потому что это просто объявление. (3) затенение x - нам нужно, чтобы x был глобальным, чтобы f мог иметь это ограничение, которое просто приводит значение x к некоторому типу T и проверяет, что результат равен true - тогда как g имеет предложение require, которое вводит новый x , затмевая глобальную. Если глобальный x = 0, вызов f невозможен. - person TamaMcGlinn; 17.07.2021

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

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

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

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

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

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

Первое требование вводит ограничение. Второй требует вводит выражение requires. Так складывается грамматика. Меня это совершенно не смущает.

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

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

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

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

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

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

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

person Community    schedule 16.01.2019

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

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

Сама оговорка "требует" чего-то требует.

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

Мех. :)

person Lightness Races in Orbit    schedule 15.01.2019
comment
Но согласно другим ответам, первый и второй requires концептуально не одно и то же (одно - это предложение, другое - выражение). На самом деле, если я правильно понимаю, два набора () в requires (requires (T x) { x + x; }) имеют очень разные значения (внешний является необязательным и всегда содержит логическое constexpr; внутренний является обязательной частью введения выражения requires и not разрешая фактические выражения). - person Max Langhof; 15.01.2019
comment
@MaxLanghof Вы хотите сказать, что требования различаются? : D - person Lightness Races in Orbit; 15.01.2019