¿Ha habido alguna vez cambios silenciosos de comportamiento en C ++ con las nuevas versiones estándar?


104

(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.

Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
Samuel Liew

3
No entiendo por qué esta pregunta está cerrada. " ¿Ha habido cambios silenciosos de comportamiento en C ++ con las nuevas versiones estándar? " Parece perfectamente enfocado y el cuerpo de la pregunta no parece desviarse de eso.
Ted Lyngmo

En mi opinión, el mayor cambio de ruptura silenciosa es la redefinición de auto. Antes de C ++ 11, auto x = ...;declaró un int. Después, declara lo que ...sea.
Raymond Chen

@RaymondChen: Este cambio solo es silencioso si estaba definiendo implícitamente int, pero diciendo explícitamente las autovariables 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 ...
einpoklum

Es cierto, por eso lo eligieron. Pero fue un gran cambio en la semántica.
Raymond Chen

Respuestas:


113

El tipo de retorno de string::datacambios de const char*a char*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.


7
Oh, ni siquiera me di cuenta de que había std::stringcambios 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.
einpoklum

9
Concebido o no, esto demuestra bastante bien un cambio en el código bien formado.
David C. Rankin

Aparte, el cambio se basa en casos de uso divertidos pero legítimos cuando cambia el contenido de un std :: string in situ, tal vez a través de funciones heredadas que operan en char *. Eso es totalmente legítimo ahora: al igual que con un vector, hay una garantía de que hay una matriz contigua subyacente que puede manipular (siempre puede hacerlo a través de referencias devueltas; ahora se hace más natural y explícito). Casos de uso son posibles, conjuntos de datos de longitud fija editables (por ejemplo, mensajes de algún tipo) que, si se basa en un std :: contenedor, retienen los servicios de la STL como la gestión del tiempo de vida, etc. copiabilidad
Peter - Restablecer Mónica

81

La respuesta a esta pregunta muestra cómo la inicialización de un vector usando un solo size_typevalor 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 deSomething 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 con v == 0luego 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 vvalores van del 0 al 9, inclusive.


@einpoklum Sin embargo, agregué un ejemplo artificial. :)
cdhowie

3
No creo que sea artificial. Los diferentes constructores a menudo actúan de manera diferente en cosas como, por ejemplo, la asignación de memoria. Simplemente reemplazó un efecto secundario con otro (E / S).
einpoklum

17
@cdhowie Nada artificial. Recientemente estuve trabajando en una clase de UUID. El constructor predeterminado generó un UUID aleatorio. No tenía idea de esta posibilidad, simplemente asumí el comportamiento de C ++ 11.
juan

5
Un ejemplo de clase del mundo real ampliamente utilizado en el que esto importaría es OpenCV cv::mat. El constructor predeterminado asigna nueva memoria, mientras que el constructor de copia crea una nueva vista a la memoria existente.
jpa

No llamaría a eso un ejemplo artificial, demuestra claramente la diferencia de comportamiento.
David Waterworth

51

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

7
@einpoklum Bueno, se dice que al menos una docena de ellos "cambian el significado" del código existente o los hacen "ejecutar de manera diferente".
cpplearner

4
¿Cómo resumiría la justificación de este cambio en particular?
Nayuki

4
@Nayuki está bastante seguro de que usar la boolversió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 los u8literales solían dar const char*pero ahora dan const char8_t*.
izquierda rotonda alrededor del

25

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.


3
Debería haber calificado la pregunta para excluir SFINAE porque esto no es exactamente lo que quise decir ... pero sí, eso es cierto, así que +1.
einpoklum

"este tipo de cosas que suceden indirectamente" resultó en un voto positivo en lugar de un voto negativo, ya que es una trampa real.
Ian Ringrose

1
Este es un buen ejemplo. Aunque OP tenía la intención de excluirlo, esta es probablemente una de las cosas más probables para causar cambios de comportamiento silenciosos en el código existente. +1
cdhowie

1
@TedLyngmo Si no puede reparar el detector, cambie la cosa detectada. ¡Tirador de Texas!
Yakk - Adam Nevraumont

15

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 .


19
Quien enseñó eso debería escribir 100 veces en la pizarra "No escribiré estructuras def". Ni siquiera deberías hacerlo en C, en mi humilde opinión. De todos modos, ese cambio no es silencioso: en el nuevo estándar, "el código válido de C ++ 2017 (usando typedef en estructuras anónimas que no son de C) puede estar mal formado" y "mal formado - el programa tiene errores de sintaxis o errores semánticos diagnosticables . Se requiere un compilador conforme a C ++ para emitir un diagnóstico " .
Peter - Reincorpora a Monica el

19
@ Peter-ReinstateMonica Bueno, siempre tengo typedefmis 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 con structpalabras clave agrega poco a la comprensión de que una letra mayúscula ( MyClass* object = myClass_create();) no transmite. Lo respeto si quieres el structen tu código. Pero no lo quiero en el mío.
cmaster - reinstalar a monica el

5
Dicho esto, cuando se programa C ++, es una buena convención usar structsolo para tipos de datos antiguos simples y classcualquier cosa que tenga funciones miembro. Pero no puede usar esa convención en C ya que no hay classen C.
cmaster - reinstale monica

1
@ Peter-ReinstateMonica Sí, bueno, no puedes adjuntar un método sintácticamente en C, pero eso no significa que C structsea ​​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 a struct, y qué invariantes están garantizados entre sus miembros. Por lo tanto, structstienden a tener funciones de miembros, implementación privada, invariantes y abstractos de sus miembros de datos. No suena como POD, ¿verdad?
cmaster - reinstalar a monica el

5
Siempre que no estén prohibidos en 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.
Cody Gray

15

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.


Bueno, técnicamente esto es cierto, pero este código era "informalmente ambiguo" para empezar debido al uso de >>esa forma.
einpoklum

11

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 charcomo 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 que chara 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 charun 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 charun 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 chara charida 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 firmado charsin 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 .


La parte de los trígrafos no es una respuesta a esta pregunta, no es un cambio silencioso. Y, IIANM, la segunda parte es un cambio de comportamiento definido por la implementación a un comportamiento estrictamente obligatorio, que tampoco es sobre lo que pregunté.
einpoklum

10

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.


1
Pensé que este "requisito" se agregó en C ++ 17, no en C ++ 11. (Ver materialización temporal .)
cdhowie

@cdhowie: Creo que tienes razón. No tenía los estándares a mano cuando escribí esto y probablemente confié demasiado en algunos de mis resultados de búsqueda.
Adrian McCarthy

Un cambio en el comportamiento definido por la implementación no cuenta como una respuesta a esta pregunta.
einpoklum

7

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


Pero no se menciona ningún cambio sobre returnType. Solo 2 sobrecarga de noticias disponibles desde C ++ 11
compilación exitosa el

¿Fue este comportamiento definido tanto en C ++ 98 como en C ++ 11? ¿O se definió el comportamiento?
einpoklum

Cuando cppreference.com tiene razón: "si ocurre un error, v se deja sin cambios (hasta C ++ 11)" Por lo tanto, el comportamiento se definió antes de C ++ 11 y se modificó.
DanRechtsaf

Según tengo entendido, el comportamiento de ss> a se definió de hecho, pero para el caso muy común en el que está leyendo una variable no inicializada, el comportamiento de c ++ 11 utilizará una variable no inicializada, que es un comportamiento indefinido. Por lo tanto, la construcción por defecto en caso de falla protege contra un comportamiento indefinido muy común.
Rasmus Damgaard Nielsen
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.