¿Por qué #include <string> evita un error de desbordamiento de pila aquí?

121

Este es mi código de muestra:

#include <iostream>
#include <string>
using namespace std;

class MyClass
{
    string figName;
public:
    MyClass(const string& s)
    {
        figName = s;
    }

    const string& getName() const
    {
        return figName;
    }
};

ostream& operator<<(ostream& ausgabe, const MyClass& f)
{
    ausgabe << f.getName();
    return ausgabe;
}

int main()
{
    MyClass f1("Hello");
    cout << f1;
    return 0;
}

Si comento #include <string>, no obtengo ningún error de compilación, supongo que se incluye de alguna manera #include <iostream>. Si hago "clic derecho -> Ir a definición" en Microsoft VS, ambos apuntan a la misma línea en el xstringarchivo:

typedef basic_string<char, char_traits<char>, allocator<char> >
    string;

Pero cuando ejecuto mi programa, aparece un error de excepción:

0x77846B6E (ntdll.dll) en OperatorString.exe: 0xC00000FD: Desbordamiento de pila (Parámetro: 0x00000001, 0x01202FC4)

¿Alguna idea de por qué aparece un error de tiempo de ejecución al comentar #include <string>? Estoy usando VS 2013 Express.

aerotransportado
fuente
44
Con la gracia de dios. funciona perfectamente en gcc, vea ideone.com/YCf4OI
v78
¿probaste visual studio con visual c ++ y comentaste como include <string>?
aire
1
@cbuchart: Aunque la pregunta ya fue respondida, creo que este es un tema lo suficientemente complejo como para que tener una segunda respuesta en diferentes palabras sea valioso. He votado para recuperar tu gran respuesta.
Carreras de ligereza en órbita
55
@Ruslan: Efectivamente, lo son. Es decir, #include<iostream>y <string>ambos podrían incluir <common/stringimpl.h>.
MSalters
3
En Visual Studio 2015, recibe una advertencia ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflowal ejecutar esta líneacl /EHsc main.cpp /Fetest.exe
CroCo,

Respuestas:

161

De hecho, comportamiento muy interesante.

Alguna idea de por qué me aparece un error de tiempo de ejecución al comentar #include <string>

Con el compilador MS VC ++, el error ocurre porque si no lo hace, no #include <string>lo habrá operator<<definido std::string.

Cuando el compilador intenta compilar ausgabe << f.getName();busca un operator<<definido para std::string. Como no se definió, el compilador busca alternativas. Hay unoperator<< definida por MyClassy los intentos del compilador para usarlo, y para utilizarlo se tiene que convertir std::stringa MyClassy esto es exactamente lo que sucede porque MyClasstiene un constructor no explícito! Entonces, el compilador termina creando una nueva instancia de tu MyClasse intenta transmitirlo nuevamente a tu flujo de salida. Esto resulta en una recursión sin fin:

 start:
     operator<<(MyClass) -> 
         MyClass::MyClass(MyClass::getName()) -> 
             operator<<(MyClass) -> ... goto start;

Para evitar el error necesitas #include <string> asegurarse de que haya un operator<<definido para std::string. También debe hacer que su MyClassconstructor sea explícito para evitar este tipo de conversión inesperada. Regla de sabiduría: haga explícitos a los constructores si toman solo un argumento para evitar la conversión implícita:

class MyClass
{
    string figName;
public:
    explicit MyClass(const string& s) // <<-- avoid implicit conversion
    {
        figName = s;
    }

    const string& getName() const
    {
        return figName;
    }
};

Parece que operator<<para la std::stringobtiene sólo cuando se define<string> se incluye (con el compilador MS) y para que compila razón, todo, sin embargo, se obtiene un comportamiento algo inesperado ya que operator<<se llama de forma recursiva para conseguir MyClassen lugar de llamar operator<<a std::string.

¿Eso significa que a través de la #include <iostream>cadena solo se incluye parcialmente?

No, la cadena está completamente incluida, de lo contrario no podría usarla.

Pavel P
fuente
19
@airborne: no es un "problema específico de Visual C ++", sino lo que puede suceder cuando no se incluye el encabezado adecuado. Cuando se usa std::stringsin #include<string>todo tipo de cosas, puede ocurrir, no limitado a un error de tiempo de compilación. Llamar a la función u operador incorrecto es aparentemente otra opción.
Bo Persson
15
Bueno, esto no es "llamar a la función u operador incorrecto"; el compilador está haciendo exactamente lo que le dijiste que hiciera. Simplemente no sabías que le estabas diciendo que hiciera esto;)
Lightness Races in Orbit
18
Usar un tipo sin incluir su correspondiente archivo de encabezado es un error. Período. ¿Podría la implementación haber facilitado la detección del error? Por supuesto. Pero ese no es un "problema" con la implementación, es un problema con el código que ha escrito.
Cody Gray
44
Las bibliotecas estándar son libres de incluir tokens que se definen en otro lugar en std dentro de sí mismos, y no están obligados a incluir el encabezado completo si definen un token.
Yakk - Adam Nevraumont
55
Es algo gracioso ver a un grupo de programadores de C ++ argumentando que el compilador y / o la biblioteca estándar deberían estar haciendo más trabajo para ayudarlos. La implementación está dentro de sus derechos aquí, de acuerdo con el estándar, como se ha señalado en numerosas ocasiones. ¿Podría usarse el "truco" para hacer esto más obvio para el programador? Claro, pero también podríamos escribir código en Java y evitar este problema por completo. ¿Por qué MSVC debe hacer visibles sus ayudantes internos? ¿Por qué un encabezado debería arrastrar un montón de dependencias que realmente no necesita? ¡Eso viola todo el espíritu del idioma!
Cody Gray
35

El problema es que su código está haciendo una recursión infinita. El operador de transmisión para std::string( std::ostream& operator<<(std::ostream&, const std::string&)) se declara en el <string>archivo de encabezado, aunque std::stringsí se declara en otro archivo de encabezado (incluido por ambos <iostream>y <string>).

Cuando no incluye <string>el compilador intenta encontrar una manera de compilar ausgabe << f.getName();.

Sucede que ha definido tanto un operador de transmisión MyClasscomo un constructor que admite a std::string, por lo que el compilador lo usa (a través de la construcción implícita ), creando una llamada recursiva.

Si declara explicitsu constructor ( explicit MyClass(const std::string& s)), su código ya no se compilará, ya que no hay forma de llamar al operador de transmisión std::stringy se verá obligado a incluir el <string>encabezado.

EDITAR

Mi entorno de prueba es VS 2010, y comenzando en el nivel de advertencia 1 ( /W1) le advierte sobre el problema:

advertencia C4717: 'operador <<': recursivo en todas las rutas de control, la función provocará un desbordamiento de la pila en tiempo de ejecución

cbuchart
fuente