¿Cuál es la razón para no usar C ++ 17 [[nodiscard]] casi en todas partes en un nuevo código?

70

C ++ 17 introduce el [[nodiscard]]atributo, que permite a los programadores marcar funciones de manera que el compilador produzca una advertencia si el llamador descarta el objeto devuelto; se puede agregar el mismo atributo a un tipo de clase completo.

He leído sobre la motivación de esta característica en la propuesta original , y sé que C ++ 20 agregará el atributo a funciones estándar como std::vector::empty, cuyos nombres no transmiten un significado inequívoco con respecto al valor de retorno.

Es una característica genial y útil. De hecho, casi parece demasiado útil. En todas partes sobre las que leo [[nodiscard]], las personas lo discuten como si simplemente lo agregaran a algunas funciones o tipos seleccionados y se olvidaran del resto. Pero, ¿por qué un valor no descartable debería ser un caso especial, especialmente al escribir código nuevo? ¿No es un valor de retorno descartado típicamente un error o al menos una pérdida de recursos?

¿Y no es uno de los principios de diseño de C ++ que el compilador debería detectar tantos errores como sea posible?

Si es así, ¿por qué no agregar [[nodiscard]]su propio código no heredado a casi todas las voidfunciones que no funcionan y casi todos los tipos de clase?

Intenté hacer eso en mi propio código, y funciona bien, excepto que es tan terriblemente detallado que comienza a sentirse como Java. Parecería mucho más natural hacer que los compiladores adviertan sobre los valores de retorno descartados por defecto, excepto en los pocos otros casos en los que marca su intención [*] .

Como no he visto ninguna discusión sobre esta posibilidad en las propuestas estándar, entradas de blog, preguntas de Stack Overflow o en cualquier otro lugar de Internet, me falta algo.

¿Por qué esa mecánica no tendría sentido en el nuevo código C ++? ¿Es la verbosidad la única razón para no usar [[nodiscard]]casi en todas partes?


[*] En teoría, es posible que tengas algo así como un [[maydiscard]]atributo, que también podría agregarse retroactivamente a funciones como printfen implementaciones de biblioteca estándar.

Christian Hackl
fuente
22
Bueno, hay muchas funciones en las que el valor de retorno puede descartarse sensiblemente. operator =por ejemplo. Y std::map::insert.
user253751
2
@immibis: cierto. La biblioteca estándar está llena de tales funciones. Pero en mi propio código, tienden a ser extremadamente raros; ¿Crees que es una cuestión de código de biblioteca versus código de aplicación?
Christian Hackl
9
"... terriblemente detallado que comienza a sentirse como Java ..." - leer esto en una pregunta sobre C ++ me hizo temblar un poco: - /
Marco13
3
@ChristianHackl Es probable que los comentarios no sean el lugar adecuado para el "intercambio de idiomas" y, por supuesto, Java también es detallado en comparación con otros idiomas. Sin embargo, los archivos de cabecera, operadores de asignación, constructores de copia y el uso adecuado de constpuede hinchar una clase de otra manera "simple" (o más bien "objeto viejo y simple de datos") considerablemente en C ++.
Marco13
2
@ Marco13: No puedo abordar tantos puntos en un comentario. Hagámoslo breve: Java no tiene archivos de encabezado, lo que reduce el recuento de archivos pero aumenta el acoplamiento; OTOH te obliga a poner cada clase de nivel superior en su propio archivo y cada función en una clase. En C ++, casi nunca implementa operadores de asignación y copia constructores usted mismo; esas funciones son más relevantes para clases de biblioteca estándar como std::vectoro std::unique_ptr, de las cuales solo necesita miembros de datos en su definición de clase. He trabajado con ambos idiomas; Java es un lenguaje aceptable, pero es más detallado.
Christian Hackl

Respuestas:

73

En el nuevo código que no necesita ser compatible con estándares más antiguos, use ese atributo siempre que sea razonable. Pero para C ++, [[nodiscard]]hace un mal defecto. Tu sugieres:

Parecería mucho más natural hacer que los compiladores adviertan sobre los valores de retorno descartados por defecto, excepto en los pocos otros casos en los que marca su intención.

Eso provocaría de repente que el código correcto existente emitiera muchas advertencias. Si bien dicho cambio podría considerarse técnicamente compatible con versiones anteriores, ya que cualquier código existente aún se compila con éxito, eso sería un gran cambio de semántica en la práctica.

Las decisiones de diseño para un lenguaje maduro existente con una base de código muy grande son necesariamente diferentes de las decisiones de diseño para un lenguaje completamente nuevo. Si este fuera un nuevo idioma, entonces la advertencia por defecto sería sensata. Por ejemplo, el lenguaje Nim requiere que los valores innecesarios se descarten explícitamente; esto sería similar a envolver cada declaración de expresión en C ++ con un reparto (void)(...).

Un [[nodiscard]]atributo es más útil en dos casos:

  • si una función no tiene efectos más allá de devolver un cierto resultado, es decir, es pura. Si no se utiliza el resultado, la llamada es ciertamente inútil. Por otro lado, descartar el resultado no sería incorrecto.

  • si se debe verificar el valor de retorno, por ejemplo, para una interfaz tipo C que devuelve códigos de error en lugar de arrojarlos. Este es el caso de uso principal. Para C ++ idiomático, eso será bastante raro.

Estos dos casos dejan un enorme espacio de funciones impuras que devuelven un valor, donde tal advertencia sería engañosa. Por ejemplo:

  • Considere un tipo de datos de cola con un .pop()método que elimina un elemento y devuelve una copia del elemento eliminado. Tal método es a menudo conveniente. Sin embargo, hay algunos casos en los que solo queremos eliminar el elemento, sin obtener una copia. Eso es perfectamente legítimo, y una advertencia no sería útil. Un diseño diferente (como std::vector) divide estas responsabilidades, pero eso tiene otras desventajas.

    Tenga en cuenta que, en algunos casos, se debe hacer una copia de todos modos, así que gracias a RVO, la devolución de la copia sería gratuita.

  • Considere interfaces fluidas, donde cada operación devuelve el objeto para que se puedan realizar más operaciones. En C ++, el ejemplo más común es el operador de inserción de flujo <<. Sería extremadamente engorroso agregar un [[nodiscard]]atributo a cada <<sobrecarga.

Estos ejemplos demuestran que hacer que el código idiomático de C ++ se compile sin advertencias bajo un lenguaje “C ++ 17 con no descarte por defecto” sería bastante tedioso.

Tenga en cuenta que su nuevo y brillante código C ++ 17 (donde puede usar estos atributos) aún puede compilarse junto con bibliotecas que se dirigen a estándares C ++ más antiguos. Esta compatibilidad con versiones anteriores es crucial para el ecosistema C / C ++. Por lo tanto, hacer que no descarte el valor predeterminado resulte en muchas advertencias para casos de uso típicos, idiomáticos, advertencias que no puede solucionar sin cambios de gran alcance en el código fuente de la biblioteca.

Podría decirse que el problema aquí no es el cambio de semántica sino que las características de cada estándar C ++ se aplican en un ámbito por unidad de compilación y no en un ámbito por archivo. Si / cuando algún estándar futuro de C ++ se aleja de los archivos de encabezado, dicho cambio sería más realista.

amon
fuente
66
He votado a favor, ya que contiene información útil. Sin embargo, aún no estoy seguro de si realmente responde la pregunta. Las funciones impuras como popo operator<<, esas estarán explícitamente marcadas por mi [[maydiscard]]atributo imaginario , por lo que no se generarán advertencias. ¿Estás diciendo que tales funciones son generalmente mucho más comunes de lo que me gustaría creer, o mucho más comunes en general que en mis propias bases de código? Su primer párrafo sobre el código existente es ciertamente cierto, pero aún así me hace preguntarme si alguien más está actualmente salpicando todo su nuevo código [[nodiscard]], y si no, por qué no.
Christian Hackl
23
@ChristianHackl: Hubo un tiempo en la historia de C ++ en el que se consideraba una buena práctica devolver valores como const. No se puede decir returns_an_int() = 0, entonces ¿por qué debería returns_a_str() = ""funcionar? C ++ 11 mostró que en realidad era una idea terrible, porque ahora todo este código prohibía los movimientos porque los valores eran constantes. Creo que la conclusión correcta de esa lección es "no depende de usted lo que hacen las personas que llaman con su resultado". Ignorar el resultado es algo perfectamente válido que las personas que llaman pueden querer hacer, y a menos que sepa que está mal (una barra muy alta), no debe bloquearlo.
GManNickG
13
Un nodiscard activado empty()también es sensato porque el nombre es bastante ambiguo: ¿es un predicado is_empty()o es un mutador clear()? Por lo general, no esperaría que dicho mutador devuelva nada, por lo que una advertencia sobre un valor de retorno puede alertar al programador de un problema. Con un nombre más claro, este atributo no sería tan necesario.
amon
13
@ChristianHackl: Como han dicho otros, creo que las funciones puras son un buen ejemplo de cuándo debería usarse. Daría un paso más y diría que debería haber un [[pure]]atributo, y debería implicar [[nodiscard]]. De esa manera, está claro por qué este resultado no debe descartarse. Pero aparte de las funciones puras, no puedo pensar en otra clase de funciones que deberían tener este comportamiento. Y la mayoría de las funciones no son puras, por lo tanto, no creo que no descarte como opción predeterminada sea la opción correcta.
GManNickG
55
@GManNickG: después de haber intentado y no haber podido depurar el código que ignoraba los códigos de error, creo firmemente que la gran mayoría de los códigos de error deben verificarse de manera predeterminada.
Mooing Duck
9

Razones por las que no lo haría [[nodiscard]]casi en todas partes:

  1. (mayor :) Esto introduciría manera demasiado ruido en mis cabeceras.
  2. (mayor :) No creo que deba hacer suposiciones especulativas sobre el código de otras personas. ¿Quieres descartar el valor de retorno que te doy? Bien, noqueate.
  3. (menor :) Estaría garantizando incompatibilidad con C ++ 14

Ahora, si lo configura por defecto, obligaría a todos los desarrolladores de bibliotecas a obligar a todos sus usuarios a no descartar los valores de retorno. Eso sería horrible O los obligas a agregar [[maydiscard]]innumerables funciones, lo que también es horrible.

einpoklum - reinstalar a Monica
fuente
0

Como ejemplo: el operador << tiene un valor de retorno que depende de la llamada, ya sea absolutamente necesario o absolutamente inútil. (std :: cout << x << y, el primero es necesario porque devuelve la secuencia, el segundo no sirve para nada). Ahora compare con printf donde todos descartan el valor de retorno que está allí para la verificación de errores, pero luego el operador << no tiene verificación de errores, por lo que no puede haber sido tan útil en primer lugar. Entonces, en ambos casos, nodiscard sería simplemente perjudicial.

gnasher729
fuente
Esto responde a una pregunta que no se hizo, es decir, "¿Cuál es la razón para no usar en [[nodiscard]]todas partes?".
Christian Hackl