Por que exigimos requer requer?

161

Um dos cantos dos conceitos do C ++ 20 é que existem certas situações nas quais você precisa escrever requires requires. Por exemplo, este exemplo de [expr.prim.req] / 3 :

Uma expressão de requisitos também pode ser usada em uma cláusula de requisitos ([temp]) como uma maneira de escrever restrições ad hoc nos argumentos do modelo, como o abaixo:

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

O primeiro requer introduz a cláusula requirida e o segundo introduz a expressão requer .

Qual é a razão técnica por trás da necessidade dessa segunda requirespalavra-chave? Por que não podemos simplesmente permitir a escrita:

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

(Nota: por favor, não responda que a gramática requires)

Barry
fonte
25
Sugestão: "Existe algo que requer requer requer?". Mais a sério, tenho um palpite de que é a mesma razão por trás noexcept(noexcept(...)).
Quentin
11
Os dois requiressão homônimos na minha opinião: eles têm a mesma aparência, soletram o mesmo, têm o mesmo cheiro, mas são intrinsecamente diferentes. Se eu sugerir uma correção, sugiro renomear uma delas.
YSC
5
@YSC - co_requires? (Desculpe, não pude resistir).
StoryTeller - Unslander Monica
122
Onde a loucura vai parar? A próxima coisa que você sabe, nós teremos long long.
Eljay
8
@StoryTeller: "Requer requer obrigatório?" teria sido ainda mais aliterativo !!
PW

Respostas:

81

É porque a gramática exige. Faz.

Uma requiresrestrição não precisa usar uma requiresexpressão. Ele pode usar qualquer expressão constante booleana mais ou menos arbitrária. Portanto, requires (foo)deve ser um legítimorequires restrição .

Uma requires expressão (aquilo que testa se certas coisas seguem certas restrições) é uma construção distinta; é apenas introduzido pela mesma palavra-chave. requires (foo f)seria o começo de uma requiresexpressão válida .

O que você deseja é que, se você usar requiresum local que aceite restrições, poderá criar uma "restrição + expressão" da requirescláusula.

Então, aqui está a pergunta: se você colocar requires (foo)em um local apropriado para a requer restrição ... até que ponto o analisador deve ir antes de perceber que essa é uma restrição requer e não uma restrição + expressão da maneira que você deseja ser estar?

Considere isto:

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

Se fooé um tipo, (foo)é uma lista de parâmetros de uma expressão requerida, e tudo no {}não é o corpo da função, mas o corpo dessa requiresexpressão. Caso contrário, fooé uma expressão em uma requirescláusula.

Bem, você poderia dizer que o compilador deve apenas descobrir o que fooé o primeiro. Mas o C ++ realmente não gosta quando o ato básico de analisar uma sequência de tokens exige que o compilador descubra o que esses identificadores significam antes que possa entender os tokens. Sim, o C ++ é sensível ao contexto, então isso acontece. Mas o comitê prefere evitá-lo sempre que possível.

Então, sim, é gramática.

Nicol Bolas
fonte
2
Faz sentido ter uma lista de parâmetros com um tipo, mas sem um nome?
precisa saber é o seguinte
3
@ Quentin: Certamente existem casos de dependência de contexto na gramática C ++. Mas o comitê realmente tenta minimizar isso, e eles definitivamente não gostam de adicionar mais .
Nicol Bolas
3
@RobertAndrzejuk: Se requiresaparecer após um <>conjunto de argumentos de modelo ou após uma lista de parâmetros de função, será uma cláusula required. Se requiresaparecer onde uma expressão é válida, é uma expressão requer. Isso pode ser determinado pela estrutura da árvore de análise, não pelo conteúdo da árvore de análise (as especificidades de como um identificador é definido seriam o conteúdo da árvore).
Nicol Bolas
6
@RobertAndrzejuk: Claro, a expressão de requisitos poderia ter usado uma palavra-chave diferente. Mas as palavras-chave têm custos enormes em C ++, pois têm o potencial de interromper qualquer programa que use o identificador que se tornou uma palavra-chave. A proposta de conceitos já introduziu duas palavras-chave: concepte requires. A introdução de um terceiro, quando o segundo seria capaz de cobrir ambos os casos, sem problemas gramaticais e poucos problemas voltados para o usuário, é apenas um desperdício. Afinal, o único problema visual é que a palavra-chave é repetida duas vezes.
Nicol Bolas
3
@RobertAndrzejuk, é uma prática ruim alinhar restrições assim, já que você não recebe subsunção como se tivesse escrito um conceito. Portanto, não é uma boa ideia usar um identificador para um recurso não recomendado de uso tão baixo.
precisa saber é o seguinte
60

A situação é exatamente análoga a noexcept(noexcept(...)). Claro, isso parece mais uma coisa ruim do que uma coisa boa, mas deixe-me explicar. :) Vamos começar com o que você já sabe:

O C ++ 11 tem " noexcept-clauses" e "-expressions" noexcept. Eles fazem coisas diferentes.

  • A noexceptcláusula diz: "Esta função deve ser exceto quando ... (alguma condição)." Ele segue uma declaração de função, pega um parâmetro booleano e causa uma alteração comportamental na função declarada.

  • Uma noexceptexpressão diz: "Compilador, por favor me diga se (alguma expressão) é nenhuma exceção". É ela própria uma expressão booleana. Não tem "efeitos colaterais" no comportamento do programa - está apenas pedindo ao compilador a resposta para uma pergunta de sim / não. "Esta expressão é exceto?"

Nós podemos um ninho noexcept-expression dentro de um noexcept-clause, mas nós normalmente considerá-lo ruim estilo para fazê-lo.

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

É considerado um estilo melhor encapsular a noexceptexpressão em uma característica de tipo.

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

O rascunho de trabalho do C ++ 2a possui " requires-clauses" e "-expressions" requires. Eles fazem coisas diferentes.

  • A requirescláusula diz: "Esta função deve participar da resolução de sobrecarga quando ... (alguma condição)." Ele segue uma declaração de função, pega um parâmetro booleano e causa uma alteração comportamental na função declarada.

  • Uma requiresexpressão diz: "Compilador, diga-me se (algum conjunto de expressões) está bem formado." É ela própria uma expressão booleana. Não tem "efeitos colaterais" no comportamento do programa - é apenas pedir ao compilador a resposta para uma pergunta de sim / não. "Esta expressão está bem formada?"

Nós podemos um ninho requires-expression dentro de um requires-clause, mas nós normalmente considerá-lo ruim estilo para fazê-lo.

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

É considerado um estilo melhor encapsular a requiresexpressão em uma característica de tipo ...

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

... ou em um conceito (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
fonte
1
Eu realmente não compro esse argumento. noexcepttem o problema que noexcept(f())poderia significar quer interpretar f()como um boolean que estamos usando para definir a especificação ou verificar se há ou não f()é noexcept. requiresnão tem essa ambiguidade porque as expressões que sua verificação de validade já precisam ser introduzidas com {}s. Depois disso, o argumento é basicamente "a gramática diz isso".
Barry
@ Barry: Veja este comentário . Parece que {}são opcionais.
Eric
1
@ Eric Não {}são opcionais, não é isso que esse comentário mostra. No entanto, esse é um ótimo comentário que demonstra a ambiguidade da análise. Provavelmente aceitar que o comentário (com alguma explicação) como uma resposta autônomo
Barry
1
requires is_nothrow_incrable_v<T>;deve serrequires is_incrable_v<T>;
Ruslan
16

Acho que a página de conceitos da cppreference explica isso. Eu posso explicar com "matemática", por assim dizer, por que isso deve ser assim:

Se você deseja definir um conceito, faça o seguinte:

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

Se você deseja declarar uma função que usa esse conceito, faça o seguinte:

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

Agora, se você não deseja definir o conceito separadamente, acho que tudo o que você precisa fazer é substituir. Pegue esta peça requires (T x) { x + x; };e substitua a Addable<T>peça, e você obterá:

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

é sobre isso que você está perguntando.

O físico quântico
fonte
4
Eu não acho que é o que a pergunta está pedindo. Isso está explicando a gramática, mais ou menos.
Passer Por
Mas por que você não pode ter template<typename T> requires (T x) { x + x; }e exigir que exigir pode ser tanto a cláusula quanto a expressão?
precisa saber é o seguinte
2
@ NathanOliver: Porque você está forçando o compilador a interpretar uma construção como outra. Uma requirescláusula -as-restringir não precisa ser uma requiresexpressão. Esse é apenas um possível uso disso.
Nicol Bolas
2
@ TheQuantumPhysicist O que eu estava falando no meu comentário é que essa resposta apenas explica a sintaxe. Não é o motivo técnico real que precisamos requires requires. Eles poderiam ter acrescentado algo à gramática para permitir, template<typename T> requires (T x) { x + x; }mas não o fizeram. Barry quer saber por que eles não fizeram
NathanOliver 15/01/19
3
Se estamos realmente brincando de encontrar a ambiguidade gramatical aqui, ok, vou morder. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; significa algo diferente se você remover um dos requireses.
Quuxplusone
12

Achei um comentário de Andrew Sutton (um dos autores do Concepts, que o implementou no gcc) ser bastante útil nesse sentido, então pensei em citá-lo aqui na íntegra:

Há não muito tempo, expressões de requerimento (a frase introduzida pelo segundo requer) não era permitida em expressões de restrição (a frase introduzida pelo primeiro requer). Só poderia aparecer nas definições de conceito. De fato, é exatamente isso que é proposto na seção desse artigo, onde essa alegação aparece.

No entanto, em 2016, houve uma proposta para relaxar essa restrição [nota do editor: P0266 ]. Observe a tacada do parágrafo 4 na seção 4 do documento. E assim nasceu requer requer.

Para dizer a verdade, eu nunca havia implementado essa restrição no GCC, portanto sempre foi possível. Eu acho que Walter pode ter descoberto isso e achou útil, levando a esse artigo.

Para que ninguém pense que eu não era sensível à escrita exige duas vezes, passei algum tempo tentando determinar se isso poderia ser simplificado. Resposta curta: não.

O problema é que existem duas construções gramaticais que precisam ser introduzidas após uma lista de parâmetros do modelo: muito comumente uma expressão de restrição (como P && Q) e ocasionalmente requisitos sintáticos (como requires (T a) { ... }). Isso é chamado de expressão requer.

O primeiro requer introduz a restrição. O segundo requer introduz a expressão requer. É assim que a gramática compõe. Não acho nada confuso.

Eu tentei, em um ponto, recolhê-los para um único requerimento. Infelizmente, isso leva a alguns problemas de análise seriamente difíceis. Você não pode dizer com facilidade, por exemplo, se a (after the require denota uma subexpressão aninhada ou uma lista de parâmetros. Não acredito que exista uma desambiguação perfeita dessas sintaxes (veja a justificativa para a sintaxe de inicialização uniforme; esse problema também existe).

Então você faz uma escolha: make exige a introdução de uma expressão (como agora) ou a introdução de uma lista parametrizada de requisitos.

Eu escolhi a abordagem atual porque, na maioria das vezes (em quase 100% das vezes), quero algo diferente de uma expressão de necessidade. E, no caso extremamente raro, eu queria uma expressão requerida para restrições ad hoc, realmente não me importo de escrever a palavra duas vezes. É um indicador óbvio que eu não desenvolvi uma abstração suficientemente sólida para o modelo. (Porque se eu tivesse, teria um nome.)

Eu poderia ter escolhido fazer com que os requisitos introduzam uma expressão de requisitos. Na verdade, isso é pior, porque praticamente todas as suas restrições começam a ficar assim:

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

Aqui, o segundo requisito é chamado de requisito aninhado; avalia sua expressão (outro código no bloco da expressão de necessidade não é avaliado). Eu acho que isso é muito pior do que o status quo. Agora, você começa a escrever requer duas vezes em todos os lugares.

Eu também poderia ter usado mais palavras-chave. Este é um problema por si só --- e não é apenas uma troca de bicicletas. Pode haver uma maneira de "redistribuir" as palavras-chave para evitar a duplicação, mas não pensei muito nisso. Mas isso realmente não muda a essência do problema.

Barry
fonte
-10

Porque você está dizendo que uma coisa A tem um requisito B e o requisito B tem um requisito C.

A coisa A requer B, que por sua vez requer C.

A cláusula "required" requer algo.

Você tem a coisa A (exigindo B (exigindo C)).

Meh. :)

Raças de leveza em órbita
fonte
4
Mas, de acordo com as outras respostas, a primeira e a segunda requiresnão são conceitualmente a mesma coisa (uma é uma cláusula, outra uma expressão). De fato, se eu entendi corretamente, os dois conjuntos de ()in requires (requires (T x) { x + x; })têm significados muito diferentes (o externo é opcional e sempre contém um cons booleano; o interno é uma parte obrigatória da introdução de uma expressão requer e não permite expressões reais).
Max Langhof
2
@MaxLanghof Você está dizendo que os requisitos diferem? : D
Lightness Races in Orbit