Por que #include <string> está impedindo um erro de estouro de pilha aqui?

121

Este é o meu código de exemplo:

#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;
}

Se eu comentar #include <string>, não obtenho nenhum erro do compilador, acho que é meio que incluído #include <iostream>. Se eu clicar com o botão direito do mouse -> Ir para definição no Microsoft VS, ambos apontam para a mesma linha no xstringarquivo:

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

Mas quando executo meu programa, recebo um erro de exceção:

0x77846B6E (ntdll.dll) no OperatorString.exe: 0xC00000FD: estouro de pilha (parâmetro: 0x00000001, 0x01202FC4)

Alguma idéia de por que recebo um erro de tempo de execução ao comentar #include <string>? Estou usando o VS 2013 Express.

transportado pelo ar
fonte
4
Com a graça de Deus. funcionando perfeitamente no gcc, consulte ideone.com/YCf4OI
v78 2/17/17
você tentou o visual studio com visual c ++ e comentou incluir <string>?
ar
1
@cbuchart: Embora a pergunta já tenha sido respondida, acho que esse é um tópico suficientemente complexo para que uma segunda resposta em palavras diferentes seja valiosa. Votei para recuperar sua ótima resposta.
Lightness Races em órbita
5
@Ruslan: Efetivamente, eles são. Ou seja, #include<iostream>e <string>ambos podem incluir <common/stringimpl.h>.
MSalters
3
No Visual Studio 2015, você recebe um aviso ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflowao executar esta linhacl /EHsc main.cpp /Fetest.exe
CroCo 2/17/17

Respostas:

161

De fato, comportamento muito interessante.

Alguma idéia de por que recebo um erro de tempo de execução ao comentar #include <string>

Com o compilador MS VC ++, o erro ocorre porque, caso contrário, #include <string>você não terá operator<<definido std::string.

Quando o compilador tenta compilar, ausgabe << f.getName();ele procura por um operator<<definido std::string. Como não foi definido, o compilador procura alternativas. Existe um operator<<definido para MyClasse o compilador tenta usá-lo, e para usá-lo, ele deve ser convertido std::stringem MyClasse é exatamente isso que acontece porque MyClasstem um construtor não explícito! Portanto, o compilador acaba criando uma nova instância sua MyClasse tenta transmiti-la novamente para o fluxo de saída. Isso resulta em uma recursão infinita:

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

Para evitar o erro, você precisa #include <string>garantir que haja um operator<<definido para std::string. Você também deve MyClassexplicitar seu construtor para evitar esse tipo de conversão inesperada. Regra de sabedoria: torne os construtores explícitos se eles tiverem apenas um argumento para evitar a conversão 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 std::stringrecebe apenas definido quando <string>está incluído (com o compilador MS) e por isso tudo compila, no entanto você tem um comportamento um tanto inesperado, como operator<<está sendo chamado de forma recursiva para MyClassem vez de chamar operator<<para std::string.

Isso significa que a #include <iostream>cadeia de caracteres é incluída apenas parcialmente?

Não, a string está totalmente incluída, caso contrário você não poderá usá-la.

Pavel P
fonte
19
@airborne - Não é um "problema específico do Visual C ++", mas o que pode acontecer quando você não inclui o cabeçalho adequado. Quando o uso std::stringsem #include<string>todos os tipos de coisas pode acontecer, não se limita a um erro de tempo de compilação. Chamar a função ou operador errado é aparentemente outra opção.
Bo Persson
15
Bem, isso não está "chamando a função ou o operador errado"; o compilador está fazendo exatamente o que você pediu. Você simplesmente não sabia que estava dizendo para fazer isso;)
Lightness Races in Orbit
18
Usar um tipo sem incluir seu arquivo de cabeçalho correspondente é um bug. Período. A implementação poderia ter facilitado a identificação do bug? Certo. Mas isso não é um "problema" com a implementação, é um problema com o código que você escreveu.
Cody Gray
4
As bibliotecas padrão são livres para incluir tokens definidos em outro lugar no std dentro de si e não precisam incluir o cabeçalho inteiro se definirem um token.
Yakk - Adam Nevraumont
5
É um pouco engraçado ver um monte de programadores de C ++ argumentando que o compilador e / ou a biblioteca padrão deveriam estar trabalhando mais para ajudá-los. A implementação está dentro dos seus direitos aqui, de acordo com o padrão, como foi apontado várias vezes. Poderia ser usado "truque" para tornar isso mais óbvio para o programador? Claro, mas também podemos escrever código em Java e evitar esse problema completamente. Por que a MSVC deve tornar seus auxiliares internos visíveis? Por que um cabeçalho deve arrastar um monte de dependências que ele realmente não precisa? Isso viola todo o espírito da linguagem!
Cody Gray
35

O problema é que seu código está fazendo uma recursão infinita. O operador de streaming para std::string( std::ostream& operator<<(std::ostream&, const std::string&)) é declarado no <string>arquivo de cabeçalho, embora std::stringele próprio seja declarado em outro arquivo de cabeçalho (incluído por ambos <iostream>e <string>).

Quando você não inclui, <string>o compilador tenta encontrar uma maneira de compilar ausgabe << f.getName();.

Acontece que você definiu um operador de streaming para MyClasse um construtor que admite a std::string; portanto, o compilador o usa (por meio de construção implícita ), criando uma chamada recursiva.

Se você declarar explicitseu construtor ( explicit MyClass(const std::string& s)), seu código não será mais compilado, pois não há como chamar o operador de streaming std::stringe você será forçado a incluir o <string>cabeçalho.

EDITAR

Meu ambiente de teste é o VS 2010 e, iniciando no nível de aviso 1 ( /W1), avisa sobre o problema:

aviso C4717: 'operator <<': recursivo em todos os caminhos de controle, a função causará estouro da pilha de tempo de execução

cbuchart
fonte