Pourquoi #include <string> empêche-t-il une erreur de débordement de pile ici?

121

Voici mon exemple de code:

#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 je commente, #include <string>je n'obtiens aucune erreur de compilation, je suppose que c'est en quelque sorte inclus #include <iostream>. Si je "clique avec le bouton droit -> Aller à la définition" dans Microsoft VS, ils pointent tous les deux vers la même ligne dans le xstringfichier:

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

Mais lorsque j'exécute mon programme, j'obtiens une erreur d'exception:

0x77846B6E (ntdll.dll) dans OperatorString.exe: 0xC00000FD: débordement de pile (paramètre: 0x00000001, 0x01202FC4)

Une idée de la raison pour laquelle j'obtiens une erreur d'exécution lors de la mise en commentaire #include <string>? J'utilise VS 2013 Express.

aéroporté
la source
4
Avec la grâce de Dieu. fonctionne parfaitement sur gcc, voir ideone.com/YCf4OI
v78
avez-vous essayé Visual Studio avec Visual C ++ et commenté include <string>?
aéroporté
1
@cbuchart: Bien que la question ait déjà reçu une réponse, je pense que c'est un sujet suffisamment complexe pour qu'il soit utile d'avoir une deuxième réponse dans des mots différents. J'ai voté pour annuler votre excellente réponse.
Courses de légèreté en orbite
5
@Ruslan: Effectivement, ils le sont. C'est-à-dire, #include<iostream>et <string>pourraient tous les deux inclure <common/stringimpl.h>.
MSalters
3
Dans Visual Studio 2015, vous recevez un avertissement ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflowavec l'exécution de cette lignecl /EHsc main.cpp /Fetest.exe
CroCo

Réponses:

161

En effet, comportement très intéressant.

Toute idée de la raison pour laquelle j'obtiens une erreur d'exécution en commentant #include <string>

Avec le compilateur MS VC ++, l'erreur se produit car si vous ne le faites pas, #include <string>vous n'aurez pas operator<<défini pour std::string.

Lorsque le compilateur essaie de compiler, ausgabe << f.getName();il recherche un operator<<fichier défini pour std::string. Puisqu'il n'a pas été défini, le compilateur recherche des alternatives. Il y a un operator<<défini pour MyClasset le compilateur essaie de l' utiliser, et de l' utiliser , il doit convertir std::stringà MyClassc'est exactement ce qui se passe parce que MyClassa un constructeur non explicite! Ainsi, le compilateur finit par créer une nouvelle instance de votre MyClasset essaie de la diffuser à nouveau dans votre flux de sortie. Cela se traduit par une récursion sans fin:

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

Pour éviter l'erreur, vous devez vous #include <string>assurer qu'il existe un operator<<fichier pour std::string. Vous devez également rendre votre MyClassconstructeur explicite pour éviter ce type de conversion inattendue. Règle de sagesse: rendre les constructeurs explicites s'ils ne prennent qu'un seul argument pour éviter la conversion implicite:

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

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

Cela ressemble à operator<<for std::stringest défini uniquement lorsque <string>est inclus (avec le compilateur MS) et pour cette raison, tout se compile, mais vous obtenez un comportement quelque peu inattendu car il operator<<est appelé récursivement pour MyClassau lieu d'appeler operator<<pour std::string.

Cela signifie-t-il que through #include <iostream>string n'est inclus qu'en partie?

Non, la chaîne est entièrement incluse, sinon vous ne pourrez pas l'utiliser.

Pavel P
la source
19
@airborne - Ce n'est pas un "problème spécifique à Visual C ++", mais ce qui peut arriver lorsque vous n'incluez pas l'en-tête approprié. Lors de l'utilisation std::stringsans #include<string>toutes sortes de choses peuvent se produire, sans se limiter à une erreur de compilation. L'appel de la mauvaise fonction ou de l'opérateur est apparemment une autre option.
Bo Persson
15
Eh bien, ce n'est pas "appeler la mauvaise fonction ou l'opérateur"; le compilateur fait exactement ce que vous lui avez dit de faire. Vous ne saviez tout simplement pas que vous lui disiez de faire cela;)
Courses de légèreté en orbite
18
Utiliser un type sans inclure son fichier d'en-tête correspondant est un bogue. Période. L'implémentation aurait-elle pu rendre le bogue plus facile à repérer? Sûr. Mais ce n'est pas un "problème" avec l'implémentation, c'est un problème avec le code que vous avez écrit.
Cody Gray
4
Les bibliothèques standard sont libres d'inclure des jetons qui sont définis ailleurs dans std à l'intérieur d'elles-mêmes, et ne sont pas obligées d'inclure l'en-tête entier si elles définissent un jeton.
Yakk - Adam Nevraumont
5
C'est un peu drôle de voir un groupe de programmeurs C ++ affirmer que le compilateur et / ou la bibliothèque standard devraient faire plus de travail pour les aider. La mise en œuvre est bien dans ses droits ici, selon la norme, comme cela a été souligné à maintes reprises. La «supercherie» pourrait-elle être utilisée pour rendre cela plus évident pour le programmeur? Bien sûr, mais nous pourrions également écrire du code en Java et éviter complètement ce problème. Pourquoi MSVC devrait-il rendre visible ses assistants internes? Pourquoi un en-tête devrait-il glisser dans un tas de dépendances dont il n'a pas réellement besoin? Cela viole tout l'esprit de la langue!
Cody Gray
35

Le problème est que votre code effectue une récursion infinie. L'opérateur de streaming pour std::string( std::ostream& operator<<(std::ostream&, const std::string&)) est déclaré dans le <string>fichier d' en- tête, bien qu'il soit std::stringlui-même déclaré dans un autre fichier d'en-tête (inclus à la fois par <iostream>et <string>).

Lorsque vous n'incluez pas, <string>le compilateur essaie de trouver un moyen de compiler ausgabe << f.getName();.

Il arrive que vous ayez défini à la fois un opérateur de streaming pour MyClasset un constructeur qui admet a std::string, de sorte que le compilateur l'utilise (via une construction implicite ), créant un appel récursif.

Si vous déclarez explicitvotre constructeur ( explicit MyClass(const std::string& s)), votre code ne se compilera plus, car il n'y a aucun moyen d'appeler l'opérateur de streaming avec std::string, et vous serez obligé d'inclure l'en- <string>tête.

ÉDITER

Mon environnement de test est VS 2010, et à partir du niveau d'avertissement 1 ( /W1), il vous avertit du problème:

avertissement C4717: 'opérateur <<': récursif sur tous les chemins de contrôle, la fonction provoquera un débordement de la pile d'exécution

cbuchart
la source