(Estoy buscando un ejemplo o dos para probar el punto, no una lista).
¿Ha ocurrido alguna vez que un cambio en el estándar C ++ (por ejemplo, de 98 a 11, 11 a 14, etc.) cambió el comportamiento del código de usuario de comportamiento definido, bien formado y existente, silenciosamente? es decir, sin advertencias o errores al compilar con la versión estándar más reciente?
Notas:
- Estoy preguntando sobre el comportamiento exigido por los estándares, no sobre las opciones del autor del implementador / compilador.
- Cuanto menos elaborado sea el código, mejor (como respuesta a esta pregunta).
- No me refiero a código con detección de versión como
#if __cplusplus >= 201103L
. - Las respuestas que involucran el modelo de memoria están bien.
c++
language-lawyer
standardization
einpoklum
fuente
fuente
auto
. Antes de C ++ 11,auto x = ...;
declaró unint
. Después, declara lo que...
sea.auto
variables de tipo were . Creo que probablemente podría contar con una mano la cantidad de personas en el mundo que escribirían ese tipo de código, excepto por los concursos de código C ofuscado ...Respuestas:
El tipo de retorno de
string::data
cambios deconst char*
achar*
en C ++ 17. Eso ciertamente podría marcar la diferencia.void func(char* data) { cout << data << " is not const\n"; } void func(const char* data) { cout << data << " is const\n"; } int main() { string s = "xyz"; func(s.data()); }
Un poco artificial, pero este programa legal cambiaría su salida de C ++ 14 a C ++ 17.
fuente
std::string
cambios para C ++ 17. En todo caso, habría pensado que los cambios de C ++ 11 podrían haber causado un cambio de comportamiento silencioso de alguna manera. +1.La respuesta a esta pregunta muestra cómo la inicialización de un vector usando un solo
size_type
valor puede resultar en un comportamiento diferente entre C ++ 03 y C ++ 11.std::vector<Something> s(10);
C ++ 03 por defecto construye un objeto temporal del tipo de elemento
Something
y copia-construye cada elemento en el vector a partir de ese temporal.C ++ 11 construye por defecto cada elemento en el vector.
En muchos (¿la mayoría?) De los casos, estos dan como resultado un estado final equivalente, pero no hay razón para que tengan que hacerlo. Depende de la implementación de
Something
los constructores por defecto / copia.Vea este ejemplo artificial :
class Something { private: static int counter; public: Something() : v(counter++) { std::cout << "default " << v << '\n'; } Something(Something const & other) : v(counter++) { std::cout << "copy " << other.v << " to " << v << '\n'; } ~Something() { std::cout << "dtor " << v << '\n'; } private: int v; }; int Something::counter = 0;
C ++ 03 construirá uno por defecto
Something
conv == 0
luego diez copiar-constructo más de que uno. Al final, el vector contiene diez objetos cuyasv
valores son del 1 al 10, inclusive.C ++ 11 construirá por defecto cada elemento. No se hacen copias. Al final, el vector contiene diez objetos cuyos
v
valores van del 0 al 9, inclusive.fuente
cv::mat
. El constructor predeterminado asigna nueva memoria, mientras que el constructor de copia crea una nueva vista a la memoria existente.La norma tiene una lista de cambios importantes en el Anexo C [diff] . Muchos de estos cambios pueden conducir a un cambio de comportamiento silencioso.
Un ejemplo:
int f(const char*); // #1 int f(bool); // #2 int x = f(u8"foo"); // until C++20: calls #1; since C++20: calls #2
fuente
bool
versión no fue un cambio intencionado per se, solo un efecto secundario de otras reglas de conversión. La verdadera intención sería detener parte de la confusión entre las codificaciones de caracteres, siendo el cambio real que losu8
literales solían darconst char*
pero ahora danconst char8_t*
.Cada vez que agregan nuevos métodos (y a menudo funciones) a la biblioteca estándar, esto sucede.
Suponga que tiene un tipo de biblioteca estándar:
struct example { void do_stuff() const; };
bastante simple. En alguna revisión estándar, se agrega un nuevo método o sobrecarga o al lado de cualquier cosa:
struct example { void do_stuff() const; void method(); // a new method };
esto puede cambiar silenciosamente el comportamiento de los programas C ++ existentes.
Esto se debe a que las capacidades de reflexión actualmente limitadas de C ++ son suficientes para detectar si tal método existe y ejecutar código diferente basado en él.
template<class T, class=void> struct detect_new_method : std::false_type {}; template<class T> struct detect_new_method< T, std::void_t< decltype( &T::method ) > > : std::true_type {};
esta es solo una forma relativamente sencilla de detectar lo nuevo
method
, hay miles de formas.void task( std::false_type ) { std::cout << "old code"; }; void task( std::true_type ) { std::cout << "new code"; }; int main() { task( detect_new_method<example>{} ); }
Lo mismo puede suceder cuando eliminas métodos de las clases.
Si bien este ejemplo detecta directamente la existencia de un método, este tipo de cosas que suceden indirectamente pueden ser menos artificiales. Como ejemplo concreto, puede tener un motor de serialización que decida si algo se puede serializar como un contenedor en función de si es iterable o si tiene datos que apuntan a bytes sin procesar y un miembro de tamaño, con uno preferido sobre el otro.
El estándar va y agrega un
.data()
método a un contenedor, y de repente el tipo cambia la ruta que usa para la serialización.Todo lo que puede hacer el estándar C ++, si no quiere congelarse, es hacer que el tipo de código que se rompe silenciosamente sea raro o de alguna manera irrazonable.
fuente
Oh chico ... El enlace que cpplearner proporcionó da miedo .
Entre otros, C ++ 20 no permitía la declaración de estructuras de estilo C de estructuras C ++.
typedef struct { void member_foo(); // Ill-formed since C++20 } m_struct;
Si te enseñaron estructuras de escritura como esa (y las personas que enseñan "C con clases" enseñan exactamente eso) estás jodido .
fuente
typedef
mis estructuras, y ciertamente no voy a desperdiciar mi tiza en eso. Esto es definitivamente una cuestión de gustos, y aunque hay personas muy influyentes (Torvalds ...) que comparten su punto de vista, otras personas como yo señalarán que todo lo que se necesita es una convención de nomenclatura para los tipos. Abarrotar el código construct
palabras clave agrega poco a la comprensión de que una letra mayúscula (MyClass* object = myClass_create();
) no transmite. Lo respeto si quieres elstruct
en tu código. Pero no lo quiero en el mío.struct
solo para tipos de datos antiguos simples yclass
cualquier cosa que tenga funciones miembro. Pero no puede usar esa convención en C ya que no hayclass
en C.struct
sea en realidad POD. De la forma en que escribo el código C, la mayoría de las estructuras solo son tocadas por el código en un solo archivo y por funciones que llevan el nombre de su clase. Básicamente es OOP sin el azúcar sintáctico. Esto me permite controlar realmente qué cambios dentro de astruct
, y qué invariantes están garantizados entre sus miembros. Por lo tanto,structs
tienden a tener funciones de miembros, implementación privada, invariantes y abstractos de sus miembros de datos. No suena como POD, ¿verdad?extern "C"
bloques, no veo ningún problema con este cambio. Nadie debería estar escribiendo estructuras en C ++. Este no es un obstáculo mayor que el hecho de que C ++ tiene una semántica diferente a la de Java. Cuando aprenda un nuevo lenguaje de programación, es posible que deba aprender algunos hábitos nuevos.Aquí hay un ejemplo que imprime 3 en C ++ 03 pero 0 en C ++ 11:
template<int I> struct X { static int const c = 2; }; template<> struct X<0> { typedef int c; }; template<class T> struct Y { static int const c = 3; }; static int const c = 4; int main() { std::cout << (Y<X< 1>>::c >::c>::c) << '\n'; }
Este cambio de comportamiento fue causado por un manejo especial de
>>
. Antes de C ++ 11,>>
siempre fue el operador de turno correcto. Con C ++ 11, también>>
puede ser parte de una declaración de plantilla.fuente
>>
esa forma.Trígrafos caídos
Los archivos de origen se codifican en un juego de caracteres físicos que se asigna de una manera definida por la implementación al juego de caracteres de origen , que se define en el estándar. Para adaptarse a las asignaciones de algunos conjuntos de caracteres físicos que no tenían de forma nativa toda la puntuación necesaria para el conjunto de caracteres de origen, el idioma definió trígrafos: secuencias de tres caracteres comunes que podrían usarse en lugar de un carácter de puntuación menos común. El preprocesador y el compilador debían manejarlos.
En C ++ 17, se eliminaron los trígrafos. Por lo tanto, algunos compiladores más nuevos no aceptarán algunos archivos de origen a menos que primero se traduzcan del juego de caracteres físicos a otro juego de caracteres físicos que se asigne uno a uno al juego de caracteres de origen. (En la práctica, la mayoría de los compiladores simplemente hacen que la interpretación de los trígrafos sea opcional). Este no es un cambio de comportamiento sutil, sino un cambio importante que evita que los archivos fuente previamente aceptables se compilen sin un proceso de traducción externo.
Más restricciones en
char
El estándar también se refiere al juego de caracteres de ejecución , que está definido por la implementación, pero debe contener al menos todo el juego de caracteres fuente más un pequeño número de códigos de control.
El estándar C ++ definido
char
como un tipo integral posiblemente sin signo que puede representar eficientemente cada valor en el juego de caracteres de ejecución. Con la representación de un abogado de idiomas, puede argumentar quechar
a debe tener al menos 8 bits.Si su implementación usa un valor sin firmar para
char
, entonces sabe que puede oscilar entre 0 y 255 y, por lo tanto, es adecuado para almacenar todos los valores de bytes posibles.Pero si su implementación usa un valor firmado, tiene opciones.
La mayoría usaría el complemento a dos, dando
char
un rango mínimo de -128 a 127. Eso es 256 valores únicos.Pero otra opción fue signo + magnitud, donde se reserva un bit para indicar si el número es negativo y los otros siete bits indican la magnitud. Eso daría
char
un rango de -127 a 127, que son solo 255 valores únicos. (Porque pierde una combinación de bits útil para representar -0).No estoy seguro de la comisión nunca explícitamente designado esto como un defecto, pero era porque no se podía confiar en la norma para garantizar un ida y vuelta desde
unsigned char
achar
ida y vuelta preservaría el valor original. (En la práctica, todas las implementaciones lo hicieron porque todas usaron el complemento a dos para los tipos integrales firmados).Solo recientemente (¿C ++ 17?) Se corrigió la redacción para garantizar la ida y vuelta. Esa solución, junto con todos los demás requisitos
char
, exige efectivamente el complemento a dos para firmadochar
sin decirlo explícitamente (incluso cuando el estándar continúa permitiendo representaciones de signo + magnitud para otros tipos integrales firmados). Hay una propuesta para requerir que todos los tipos integrales firmados usen el complemento de dos, pero no recuerdo si llegó a C ++ 20.Entonces, este es algo opuesto a lo que está buscando porque le da una corrección retroactiva a un código demasiado presuntuoso que anteriormente era
incorrecto.fuente
No estoy seguro de si consideraría esto un cambio importante para corregir el código, pero ...
Antes de C ++ 11, a los compiladores se les permitía, pero no se les exigía, eludir copias en determinadas circunstancias, incluso cuando el constructor de copias tiene efectos secundarios observables. Ahora tenemos elisión de copia garantizada. El comportamiento básicamente pasó de definido por la implementación a obligatorio.
Esto significa que los efectos secundarios de su constructor de copias pueden haber ocurrido con versiones anteriores, pero nunca ocurrirán con las más nuevas. Se podría argumentar que el código correcto no debería depender de los resultados definidos por la implementación, pero no creo que eso sea lo mismo que decir que dicho código es incorrecto.
fuente
El comportamiento al leer datos (numéricos) de una secuencia y la lectura falla, se cambió desde c ++ 11.
Por ejemplo, leer un número entero de una secuencia, mientras que no contiene un número entero:
#include <iostream> #include <sstream> int main(int, char **) { int a = 12345; std::string s = "abcd"; // not an integer, so will fail std::stringstream ss(s); ss >> a; std::cout << "fail = " << ss.fail() << " a = " << a << std::endl; // since c++11: a == 0, before a still 12345 }
Dado que c ++ 11 establecerá el entero de lectura en 0 cuando falle; en c ++ <11, el número entero no se cambió. Dicho esto, gcc, incluso cuando se obliga al estándar a volver a c ++ 98 (con -std = c ++ 98) siempre muestra un nuevo comportamiento al menos desde la versión 4.4.7.
(En mi opinión, el comportamiento anterior era en realidad mejor: ¿por qué cambiar el valor a 0, que es válido por sí mismo, cuando no se puede leer nada?)
Referencia: consulte https://en.cppreference.com/w/cpp/locale/num_get/get
fuente