¿Por qué requerimos requiere requiere?

161

Una de las esquinas de los conceptos de C ++ 20 es que hay ciertas situaciones en las que tienes que escribir requires requires. Por ejemplo, este ejemplo de [expr.prim.req] / 3 :

Una expresión require también se puede usar en una cláusula require ([temp]) como una forma de escribir restricciones ad hoc en argumentos de plantilla como el siguiente:

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

El primero requiere introduce la cláusula require , y el segundo introduce la expresión require .

¿Cuál es la razón técnica detrás de necesitar esa segunda requirespalabra clave? ¿Por qué no podemos simplemente permitir escribir:

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

(Nota: por favor no responda que la gramática requireses)

Barry
fuente
25
Sugerencia: "¿Hay algo que requiere requiere requiere?". Más en serio, tengo el presentimiento de que es la misma razón detrás noexcept(noexcept(...)).
Quentin
11
Los dos requiresson homónimos en mi opinión: se ven iguales, deletrean igual, huelen igual, pero son intrínsecamente diferentes. Si tuviera que sugerir una solución, sugeriría cambiar el nombre de uno de ellos.
YSC
55
@YSC - co_requires? (Lo siento, no pude resistir).
StoryTeller - Unslander Monica
122
¿Dónde se detendrá la locura? Lo siguiente que sabes, lo haremos long long.
Eljay
8
@StoryTeller: "¿Requiere requiere requerido?" habría sido aún más aliterativo !!
PW

Respuestas:

81

Es porque la gramática lo requiere. Lo hace.

Una requiresrestricción no tiene que usar una requiresexpresión. Puede usar cualquier expresión constante booleana más o menos arbitraria. Por lo tanto, requires (foo)debe ser una requiresrestricción legítima .

Una requires expresión (esa cosa que prueba si ciertas cosas siguen ciertas restricciones) es una construcción distinta; solo se introduce con la misma palabra clave. requires (foo f)Sería el comienzo de una requiresexpresión válida .

Lo que quiere es que si lo usa requiresen un lugar que acepta restricciones, debería poder hacer una "restricción + expresión" fuera de la requirescláusula.

Así que aquí está la pregunta: si coloca requires (foo)en un lugar que sea apropiado para una restricción requerida ... ¿qué tan lejos tiene que ir el analizador antes de que pueda darse cuenta de que esta es una restricción requerida en lugar de una restricción + expresión de la manera que lo desea? ¿ser - estar?

Considera esto:

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

Si fooes un tipo, entonces (foo)es una lista de parámetros de una expresión requerida, y todo en el {}no es el cuerpo de la función sino el cuerpo de esa requiresexpresión. De lo contrario, fooes una expresión en una requirescláusula.

Bueno, se podría decir que el compilador debería descubrir qué fooes primero. Pero a C ++ realmente no le gusta cuando el acto básico de analizar una secuencia de tokens requiere que el compilador descubra qué significan esos identificadores antes de que pueda dar sentido a los tokens. Sí, C ++ es sensible al contexto, por lo que esto sucede. Pero el comité prefiere evitarlo cuando sea posible.

Entonces sí, es gramática.

Nicol Bolas
fuente
2
¿Tiene sentido tener una lista de parámetros con un tipo pero sin nombre?
NathanOliver
3
@Quentin: Ciertamente hay casos de dependencia de contexto en la gramática de C ++. Pero el comité realmente trata de minimizar eso, y definitivamente no les gusta agregar más .
Nicol Bolas
3
@RobertAndrzejuk: si requiresaparece después de un <>conjunto de argumentos de plantilla o después de una lista de parámetros de función, entonces es una cláusula obligatoria. Si requiresaparece donde una expresión es válida, entonces es una expresión obligatoria. Esto puede determinarse por la estructura del árbol de análisis, no por el contenido del árbol de análisis (los detalles de cómo se define un identificador serían los contenidos del árbol).
Nicol Bolas
66
@RobertAndrzejuk: Claro, la expresión requerida podría haber usado una palabra clave diferente. Pero las palabras clave tienen costos enormes en C ++, ya que tienen el potencial de romper cualquier programa que haya utilizado el identificador que se ha convertido en una palabra clave. La propuesta de conceptos ya introdujo dos palabras clave: concepty requires. Introducir un tercero, cuando el segundo sería capaz de cubrir ambos casos sin problemas gramaticales y con pocos problemas para el usuario, es un desperdicio. Después de todo, el único problema visual es que la palabra clave se repite dos veces.
Nicol Bolas
3
@RobertAndrzejuk es una mala práctica de todos modos alinear restricciones como esa, ya que no obtienes subsunción como si hubieras escrito un concepto. Por lo tanto, no es una buena idea tomar un identificador para una función no recomendada de tan bajo uso.
Rakete1111
60

La situación es exactamente análoga a noexcept(noexcept(...)). Claro, esto suena más como algo malo que como algo bueno, pero déjame explicarte. :) Comenzaremos con lo que ya sabes:

C ++ 11 tiene " noexcept-cláusulas" y " noexcept-expresiones". Ellos hacen cosas diferentes.

  • Una noexceptcláusula dice: "Esta función no debe ser excepto cuando ... (alguna condición)". Va en una declaración de función, toma un parámetro booleano y provoca un cambio de comportamiento en la función declarada.

  • Una expresión noexcept-expresa, "Compilador, por favor dime si (alguna expresión) no es excepto". Es en sí misma una expresión booleana. No tiene "efectos secundarios" en el comportamiento del programa, solo está pidiendo al compilador la respuesta a una pregunta de sí / no. "¿Esta expresión no es la excepción?"

Nos puede anidar un noexcept-expresión dentro de un noexcept-clause, pero por lo general consideramos que es un mal estilo para hacerlo.

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

Se considera un mejor estilo para encapsular la noexceptexpresión en un rasgo 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

El borrador de trabajo de C ++ 2a tiene " requirescláusulas" y "expresiones" requires. Ellos hacen cosas diferentes.

  • Una requirescláusula dice: "Esta función debe participar en la resolución de sobrecarga cuando ... (alguna condición)". Va en una declaración de función, toma un parámetro booleano y provoca un cambio de comportamiento en la función declarada.

  • Una expresión- requiresdice: "Compilador, por favor dígame si (algún conjunto de expresiones) está bien formado". Es en sí misma una expresión booleana. No tiene "efectos secundarios" en el comportamiento del programa, solo está pidiendo al compilador la respuesta a una pregunta de sí / no. "¿Esta expresión está bien formada?"

Nos puede anidar un requires-expresión dentro de un requires-clause, pero por lo general consideramos que es un mal estilo para hacerlo.

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

Se considera mejor estilo encapsular la requiresexpresión en un rasgo 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

... o en un concepto (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
fuente
1
Realmente no compro este argumento. noexcepttiene el problema de que noexcept(f())podría significar o bien interpretar f()como un valor lógico que estamos usando para establecer la especificación o comprobar si es o no f()es noexcept. requiresno tiene esta ambigüedad porque las expresiones que verifican su validez ya tienen que ser introducidas con {}s. Después de eso, el argumento es básicamente "la gramática lo dice".
Barry
@Barry: mira este comentario . Parece que {}son opcionales.
Eric
1
@Eric The {}no son opcionales, eso no es lo que muestra ese comentario. Sin embargo, ese es un gran comentario que demuestra la ambigüedad del análisis. Probablemente aceptaría ese comentario (con alguna explicación) como una respuesta independiente
Barry
1
requires is_nothrow_incrable_v<T>;debería serrequires is_incrable_v<T>;
Ruslan
16

Creo que la página de conceptos de cppreference explica esto. Puedo explicar con "matemáticas", por decir, por qué esto debe ser así:

Si desea definir un concepto, haga esto:

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

Si desea declarar una función que usa ese concepto, haga esto:

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

Ahora, si no quieres definir el concepto por separado, supongo que todo lo que tienes que hacer es alguna sustitución. Tome esta parte requires (T x) { x + x; };y reemplácela Addable<T>, y obtendrá:

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

que es lo que preguntas.

El físico cuántico
fuente
44
No creo que sea lo que pregunta. Esto explica la gramática, más o menos.
Pasador antes del
Pero, ¿por qué no puede tener template<typename T> requires (T x) { x + x; }y exigir que require pueda ser tanto la cláusula como la expresión?
NathanOliver
2
@NathanOliver: Porque estás obligando al compilador a interpretar una construcción como otra. Una requirescláusula -as-restricción no tiene que ser una requires-expresión. Eso es simplemente un posible uso de él.
Nicol Bolas
2
@TheQuantumPhysicist A lo que me refería con mi comentario es que esta respuesta solo explica la sintaxis. No qué razón técnica real tenemos que hacer requires requires. Podrían haber agregado algo a la gramática para permitir, template<typename T> requires (T x) { x + x; }pero no lo hicieron. Barry quiere saber por qué no lo hicieron
NathanOliver
3
Si realmente estamos jugando a encontrar la ambigüedad gramatical aquí, OK, voy a morder. godbolt.org/z/i6n8kM template<class T> void f(T) requires requires(T (x)) { (void)x; }; significa algo diferente si elimina uno de los requireses.
Quuxplusone
12

He encontrado un comentario de Andrew Sutton (uno de los autores conceptos, que se implementó en gcc) a ser muy útil en este sentido, así que pensé que acababa de citar aquí en su casi totalidad:

No hace mucho tiempo, las expresiones obligatorias (la frase introducida por el segundo requerimiento) no estaban permitidas en las expresiones de restricción (la frase introducida por el primero requiere). Solo podría aparecer en definiciones de conceptos. De hecho, esto es exactamente lo que se propone en la sección de ese documento donde aparece ese reclamo.

Sin embargo, en 2016, hubo una propuesta para relajar esa restricción [Nota del editor: P0266 ]. Obsérvese el tachado del párrafo 4 en la sección 4 del documento. Y así nació nació requiere requiere.

A decir verdad, nunca había implementado esa restricción en GCC, por lo que siempre había sido posible. Creo que Walter pudo haber descubierto eso y lo encontró útil, lo que lleva a ese documento.

Para que nadie piense que no era sensible a la escritura requiere dos veces, pasé algún tiempo tratando de determinar si eso podría simplificarse. Respuesta corta: no.

El problema es que hay dos construcciones gramaticales que deben introducirse después de una lista de parámetros de plantilla: muy comúnmente, una expresión de restricción (like P && Q) y, en ocasiones, requisitos sintácticos (like requires (T a) { ... }). Eso se llama una expresión obligatoria.

El primero requiere introduce la restricción. El segundo requiere introduce la expresión requerida. Así es como se compone la gramática. No lo encuentro confuso en absoluto.

Traté, en un punto, de colapsar esto a una sola necesidad. Desafortunadamente, eso lleva a algunos problemas de análisis muy difíciles. No se puede decir fácilmente, por ejemplo, si un (after the require denota una subexpresión anidada o una lista de parámetros. No creo que haya una desambiguación perfecta de esas sintaxis (vea la justificación de la sintaxis de inicialización uniforme; este problema también está ahí).

Por lo tanto, elija: make requiere introducir una expresión (como lo hace ahora) o hacer que presente una lista parametrizada de requisitos.

Elegí el enfoque actual porque la mayoría de las veces (como en casi el 100% de las veces), quiero algo más que una expresión obligatoria. Y en el caso extremadamente raro que quería una expresión obligatoria para restricciones ad hoc, realmente no me importa escribir la palabra dos veces. Es un indicador obvio de que no he desarrollado una abstracción suficientemente sólida para la plantilla. (Porque si lo tuviera, tendría un nombre).

Podría haber elegido hacer que los requisitos introduzcan una expresión de requisitos. Eso es realmente peor, porque prácticamente todas sus limitaciones comenzarían a verse así:

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

Aquí, el segundo requerimiento se llama requisito anidado; evalúa su expresión (no se evalúa otro código en el bloque de la expresión requerida). Creo que esto es mucho peor que el status quo. Ahora, puedes escribir requiere dos veces en todas partes.

También podría haber usado más palabras clave. Este es un problema en sí mismo, y no se trata solo de arrojar bicicletas. Puede haber una forma de "redistribuir" palabras clave para evitar la duplicación, pero no he pensado mucho en eso. Pero eso realmente no cambia la esencia del problema.

Barry
fuente
-10

Porque usted está diciendo que una cosa A tiene un requisito B, y el requisito B tiene un requisito C.

La cosa A requiere B, que a su vez requiere C.

La cláusula "requiere" en sí misma requiere algo.

Tienes la cosa A (que requiere B (que requiere C)).

Meh :)

Carreras de ligereza en órbita
fuente
44
Pero según las otras respuestas, el primero y el segundo requiresno son conceptualmente lo mismo (uno es una cláusula, otro una expresión). De hecho, si entiendo correctamente, los dos conjuntos de ()in requires (requires (T x) { x + x; })tienen significados muy diferentes (el exterior es opcional y siempre contiene un constexpr booleano; el interior es una parte obligatoria de la introducción de una expresión obligatoria y no permite expresiones reales).
Max Langhof
2
@MaxLanghof ¿Estás diciendo que los requisitos difieren? : D
ligereza corre en órbita el