Dlaczego wymagamy wymagań?

161

Jednym z rogów koncepcji C ++ 20 jest to, że są pewne sytuacje, w których musisz pisać requires requires. Na przykład ten przykład z [wyr.prim.req] / 3 :

Wymaga ekspresja może być również stosowany w wymaga-klauzula ([temp]) jako sposób pisania ograniczenia ad hoc dotyczący argumentów szablonu, takich jak ten poniżej:

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

Pierwsza wymaga wprowadza klauzulę wymaga , a druga wprowadza wyrażenie wymaga .

Jaki jest techniczny powód, dla którego potrzebne jest drugie requiressłowo kluczowe? Dlaczego nie możemy po prostu pozwolić na pisanie:

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

(Uwaga: proszę nie odpowiadać, że gramatyka requiresto)

Barry
źródło
25
Sugestia: „Czy jest coś, co wymaga wymagań?”. Mówiąc poważnie, mam przeczucie, że to ten sam powód noexcept(noexcept(...)).
Quentin
11
requiresMoim zdaniem oba są homonimami: wyglądają tak samo, pisują tak samo, pachną tak samo, ale są z natury różne. Gdybym miał zasugerować poprawkę, sugerowałbym zmianę nazwy jednego z nich.
YSC
5
@YSC - co_requires? (Przepraszam, nie mogłem się oprzeć).
StoryTeller - Unslander Monica
122
Gdzie skończy się szaleństwo? Następna rzecz, jaką wiesz, będziemy mieć long long.
Eljay
8
@StoryTeller: „Wymaga wymaga?” byłoby jeszcze bardziej aliteracyjne !!
PW

Odpowiedzi:

81

Dzieje się tak, ponieważ wymaga tego gramatyka. To robi.

requiresOgraniczenie nie trzeba używać requireswyrazu. Może używać dowolnego mniej lub bardziej dowolnego logicznego wyrażenia stałego. Dlatego requires (foo)musi być uzasadnionym requiresograniczeniem.

requires Ekspresji (to sprawa, że sprawdza, czy pewne rzeczy następują pewne ograniczenia) jest odrębną Konstrukt; jest po prostu wprowadzany tym samym słowem kluczowym. requires (foo f)byłby początkiem prawidłowego requireswyrażenia.

To, czego chcesz, to to, że jeśli używasz requiresw miejscu, które akceptuje ograniczenia, powinieneś być w stanie zrobić „ograniczenie + wyrażenie” z requiresklauzuli.

Oto więc pytanie: jeśli umieścisz requires (foo)miejsce, które jest odpowiednie dla ograniczenia wymagań ... jak daleko parser musi się posunąć, zanim zda sobie sprawę, że jest to raczej ograniczenie wymagające niż ograniczenie + wyrażenie tak, jak chcesz być?

Rozważ to:

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

Jeśli foojest typem, to (foo)jest listą parametrów wyrażenia wymaganego, a wszystko w elemencie {}nie jest treścią funkcji, ale treścią requireswyrażenia. W przeciwnym razie foojest wyrażeniem w requiresklauzuli.

Cóż, można powiedzieć, że kompilator powinien po prostu dowiedzieć się, co foojest pierwsze. Ale C ++ naprawdę nie lubi, kiedy podstawowa czynność parsowania sekwencji tokenów wymaga, aby kompilator zorientował się, co oznaczają te identyfikatory, zanim będzie mógł nadać sens tokenom. Tak, C ++ jest zależny od kontekstu, więc tak się dzieje. Ale komisja woli tego unikać, jeśli to możliwe.

Więc tak, to gramatyka.

Nicol Bolas
źródło
2
Czy warto mieć listę parametrów z typem, ale bez nazwy?
NathanOliver
3
@Quentin: W gramatyce C ++ są z pewnością przypadki zależności kontekstowych. Ale komitet naprawdę stara się to zminimalizować i zdecydowanie nie lubi dodawać kolejnych .
Nicol Bolas
3
@RobertAndrzejuk: Jeśli requirespojawia się po <>zestawie argumentów szablonu lub po liście parametrów funkcji, to jest to klauzula wymaga. Jeśli requirespojawia się tam, gdzie wyrażenie jest prawidłowe, jest to wyrażenie wymagające. Można to określić na podstawie struktury drzewa analizy, a nie zawartości drzewa analizy (specyfiką definiowania identyfikatora byłaby zawartość drzewa).
Nicol Bolas
6
@RobertAndrzejuk: Jasne, wyrażenie require mogło użyć innego słowa kluczowego. Ale słowa kluczowe mają ogromne koszty w C ++, ponieważ mogą zepsuć każdy program, który używał identyfikatora, który stał się słowem kluczowym. Propozycja koncepcji wprowadziła już dwa słowa kluczowe: concepti requires. Wprowadzenie trzeciego, kiedy drugi byłby w stanie objąć oba przypadki bez problemów gramatycznych i kilku problemów napotykanych przez użytkowników, jest po prostu marnotrawstwem. W końcu jedynym problemem wizualnym jest to, że słowo kluczowe powtarza się dwukrotnie.
Nicol Bolas
3
@RobertAndrzejuk i tak złą praktyką jest wprowadzanie takich ograniczeń, ponieważ nie uzyskuje się podporządkowania tak, jakbyś napisał koncepcję. Więc wzięcie identyfikatora dla tak niskiego użycia, niezalecanej funkcji, nie jest dobrym pomysłem.
Rakete1111
60

Sytuacja jest dokładnie analogiczna do noexcept(noexcept(...)). Jasne, brzmi to bardziej jak zła rzecz niż dobra, ale pozwól mi wyjaśnić. :) Zaczniemy od tego, co już wiesz:

C ++ 11 ma „ noexcept-clauses” i „ noexcept-expressions”. Robią różne rzeczy.

  • noexceptKlauzula- A mówi: „Ta funkcja nie powinna być wyjątkiem, gdy ... (jakiś warunek)”. Przechodzi do deklaracji funkcji, przyjmuje parametr boolowski i powoduje zmianę zachowania w zadeklarowanej funkcji.

  • A noexcept-expression mówi: „Kompilatorze, powiedz mi, czy (jakieś wyrażenie) jest noexcept”. Samo jest wyrażeniem logicznym. Nie ma żadnych "skutków ubocznych" w zachowaniu programu - po prostu prosi kompilator o odpowiedź na pytanie tak / nie. "Czy to wyrażenie noexcept?"

My może zagnieździć się noexcept-expression wewnątrz noexcept-clause, ale zazwyczaj uważają ją złe stylu, aby to zrobić.

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

Uważa się, że lepszy styl zawiera noexcept-wyrażenie w cechach typu.

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

Robocza requireswersja robocza C ++ 2a ma „ -clauses” i „ requires-expressions”. Robią różne rzeczy.

  • A requires-clause mówi: „Ta funkcja powinna brać udział w rozwiązywaniu problemu przeciążenia, gdy ... (jakiś warunek)”. Przechodzi do deklaracji funkcji, przyjmuje parametr boolowski i powoduje zmianę zachowania w zadeklarowanej funkcji.

  • A requires-expression mówi: „Kompilatorze, powiedz mi, czy (jakiś zestaw wyrażeń) jest poprawnie sformułowany”. Samo jest wyrażeniem logicznym. Nie ma żadnych "skutków ubocznych" w zachowaniu programu - po prostu prosi kompilator o odpowiedź na pytanie tak / nie. "Czy to wyrażenie jest dobrze sformułowane?"

My może zagnieździć się requires-expression wewnątrz requires-clause, ale zazwyczaj uważają ją złe stylu, aby to zrobić.

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

Uważa się, że lepszy styl hermetyzowania requireswyrażenia w cechach typu ...

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

... lub w koncepcji (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
Quuxplusone
źródło
1
Naprawdę nie kupuję tego argumentu. noexceptma problem, który noexcept(f())może oznaczać albo zinterpretować f()jako wartość logiczną, której używamy do ustawienia specyfikacji, albo sprawdzić, czy tak f()jest noexcept. requiresnie ma tej niejednoznaczności, ponieważ wyrażenia, które sprawdza poprawność, muszą być już wprowadzone za pomocą {}s. Następnie argument jest w zasadzie „tak mówi gramatyka”.
Barry,
@Barry: Zobacz ten komentarz . Wygląda na {}to, że są opcjonalne.
Eric
1
@Eric Te {}nie są opcjonalne, nie to pokazuje ten komentarz. Jest to jednak świetny komentarz pokazujący niejednoznaczność analizowania. Prawdopodobnie zaakceptowałby ten komentarz (z pewnym wyjaśnieniem) jako samodzielną odpowiedź
Barry
1
requires is_nothrow_incrable_v<T>;powinno byćrequires is_incrable_v<T>;
Ruslan
16

Myślę, że strona pojęć cppreference wyjaśnia to. Mogę wyjaśnić za pomocą „matematyki”, że tak powiem, dlaczego to musi wyglądać tak:

Jeśli chcesz zdefiniować pojęcie, robisz to:

template<typename T>
concept Addable = requires (T x) { x + x; }; // requires-expression

Jeśli chcesz zadeklarować funkcję używającą tego pojęcia, wykonaj następujące czynności:

template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }

Jeśli nie chcesz osobno definiować tego pojęcia, myślę, że wszystko, co musisz zrobić, to jakąś substytucję. Weź tę część requires (T x) { x + x; };i wymień Addable<T>część, a otrzymasz:

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

o to właśnie pytasz.

Fizyk kwantowy
źródło
4
Nie sądzę, o co chodzi w tym pytaniu. To mniej więcej wyjaśnia gramatykę.
Przechodzień
Ale dlaczego nie możesz mieć template<typename T> requires (T x) { x + x; }i wymagać, aby wymaganie mogło być zarówno klauzulą, jak i wyrażeniem?
NathanOliver
2
@NathanOliver: Ponieważ zmuszasz kompilator do interpretowania jednej konstrukcji jako drugiej. requiresKlauzula -as-ograniczenia nie muszą być requires-expression. To tylko jedno z możliwych zastosowań.
Nicol Bolas
2
@TheQuantumPhysicist To, do czego doszedłem z moim komentarzem, to to, że ta odpowiedź tylko wyjaśnia składnię. Nie z jakiego technicznego powodu mamy do czynienia requires requires. Mogli dodać coś do gramatyki, template<typename T> requires (T x) { x + x; }ale nie zrobili tego. Barry chce wiedzieć, dlaczego tego nie zrobili
NathanOliver
3
Jeśli naprawdę bawimy się tutaj w znajdź-niejednoznaczność-gramatyki, OK, ugryzę. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; oznacza coś innego, jeśli usuniesz jeden z requireses.
Quuxplusone
12

Znalazłem komentarz Andrew Suttona (jednego z autorów Concepts, który zaimplementował go w gcc), który był bardzo pomocny w tym względzie, więc pomyślałem, że zacytuję go tutaj w całości:

Nie tak dawno temu wyrażenia wymagań (fraza wprowadzona przez drugie wymagania) nie były dozwolone w wyrażeniach ograniczających (fraza wprowadzona przez pierwsze wymagania). Mogło pojawić się tylko w definicjach pojęć. W rzeczywistości jest to dokładnie to, co zaproponowano w części tego dokumentu, w której pojawia się to twierdzenie.

Jednak w 2016 r. Pojawiła się propozycja złagodzenia tego ograniczenia [przyp. Red . : P0266 ]. Zwróć uwagę na przekreślenie akapitu 4 w sekcji 4 pracy. I tak narodził się wymaga wymagań.

Prawdę mówiąc, nigdy nie wdrożyłem tego ograniczenia w GCC, więc zawsze było to możliwe. Myślę, że Walter mógł to odkryć i uznał to za przydatne, co doprowadziło do tego artykułu.

Żeby ktoś nie pomyślał, że nie jestem wrażliwy na pisanie, wymaga dwóch rzeczy, spędziłem trochę czasu próbując ustalić, czy można to uprościć. Krótka odpowiedź: nie.

Problem polega na tym, że istnieją dwie konstrukcje gramatyczne, które należy wprowadzić po liście parametrów szablonu: bardzo często wyrażenie ograniczające (jak P && Q) i czasami wymagania składniowe (podobne requires (T a) { ... }). Nazywa się to wyrażeniem wymagania.

Pierwszy wymóg wprowadza ograniczenie. Drugi wymaga wprowadza wyrażenie wymagań. Tak po prostu komponuje się gramatyka. Nie wydaje mi się to wcale mylące.

W pewnym momencie próbowałem złożyć je na pojedyncze wymagania. Niestety prowadzi to do poważnych problemów z analizowaniem. Nie można łatwo stwierdzić, na przykład, czy (po wymaganiu oznacza zagnieżdżone wyrażenie podrzędne lub listę parametrów. Nie wierzę, że istnieje idealne ujednoznacznienie tych składni (patrz uzasadnienie jednolitej składni inicjalizacji; ten problem też tam jest).

Dokonujesz więc wyboru: make wymaga wprowadzenia wyrażenia (tak jak teraz) lub sprawi, że wprowadzi sparametryzowaną listę wymagań.

Wybrałem obecne podejście, ponieważ przez większość czasu (prawie w 100%) chcę czegoś innego niż wyrażenie wymagania. A w niezwykle rzadkim przypadku, gdy chciałem wyrażenia wymagań dla ograniczeń ad hoc, naprawdę nie mam nic przeciwko dwukrotnemu napisaniu tego słowa. To oczywista wskazówka, że ​​nie opracowałem wystarczająco solidnej abstrakcji dla szablonu. (Bo gdybym miał, miałoby imię.)

Mogłem zdecydować, aby wymagania wprowadziły wyrażenie wymagań. W rzeczywistości jest to gorsze, ponieważ praktycznie wszystkie twoje ograniczenia zaczęłyby wyglądać tak:

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

Tutaj drugie wymaganie nazywa się wymaganiem zagnieżdżonym; oblicza swoje wyrażenie (inny kod w bloku wyrażenia wymagania nie jest oceniany). Myślę, że jest to o wiele gorsze niż status quo. Teraz możesz pisać wymaga dwukrotnie wszędzie.

Mogłem też użyć więcej słów kluczowych. Jest to problem sam w sobie - i nie chodzi tylko o porzucanie roweru. Może istnieć sposób na „redystrybucję” słów kluczowych, aby uniknąć powielania, ale nie zastanawiałem się nad tym poważnie. Ale to nie zmienia istoty problemu.

Barry
źródło
-10

Ponieważ mówisz, że rzecz A ma wymaganie B, a wymaganie B ma wymaganie C.

Rzecz A wymaga B, która z kolei wymaga C.

Sama klauzula „wymaga” czegoś wymaga.

Masz rzecz A (wymagająca B (wymagająca C)).

Meh. :)

Lekkość wyścigów na orbicie
źródło
4
Ale według innych odpowiedzi pierwsza i druga requiresnie są koncepcyjnie tym samym (jedna jest klauzulą, druga wyrażeniem). W rzeczywistości, jeśli dobrze rozumiem, te dwa zbiory ()in requires (requires (T x) { x + x; })mają bardzo różne znaczenia (zewnętrzna jest opcjonalna i zawsze zawiera boolean constexpr; wewnętrzna jest obowiązkową częścią wprowadzenia wymaganego wyrażenia i nie zezwala na rzeczywiste wyrażenia).
Max Langhof
2
@MaxLanghof Czy twierdzisz, że wymagania są różne? : D
Wyścigi lekkości na orbicie