Dlaczego #include <string> zapobiega tutaj błędowi przepełnienia stosu?

121

Oto mój przykładowy kod:

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

Jeśli wykomentuję, #include <string>nie otrzymam żadnego błędu kompilatora, myślę, że jest to rodzaj dołączenia #include <iostream>. Jeśli kliknę prawym przyciskiem myszy -> Idź do definicji w programie Microsoft VS, oba wskazują ten sam wiersz w xstringpliku:

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

Ale kiedy uruchamiam program, pojawia się błąd wyjątku:

0x77846B6E (ntdll.dll) w OperatorString.exe: 0xC00000FD: przepełnienie stosu (parametr: 0x00000001, 0x01202FC4)

Masz jakieś pojęcie, dlaczego podczas komentowania pojawia się błąd wykonania #include <string>? Używam VS 2013 Express.

samolotowy
źródło
4
Z łaską Bożą. działa idealnie na gcc, zobacz ideone.com/YCf4OI
v78
czy wypróbowałeś Visual Studio z Visual C ++ i skomentowałeś include <string>?
powietrzu
1
@cbuchart: Chociaż odpowiedź na to pytanie została już udzielona, ​​myślę, że jest to na tyle złożony temat, że posiadanie drugiej odpowiedzi w innych słowach jest cenne. Głosowałem za cofnięciem Twojej świetnej odpowiedzi.
Wyścigi lekkości na orbicie
5
@Ruslan: W rzeczywistości tak. To znaczy, #include<iostream>i <string>oba mogą obejmować <common/stringimpl.h>.
MSalters
3
W programie Visual Studio 2015 ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflowcl /EHsc main.cpp /Fetest.exe
pojawia

Odpowiedzi:

161

Rzeczywiście, bardzo ciekawe zachowanie.

Każdy pomysł, dlaczego otrzymuję błąd w czasie wykonywania podczas komentowania #include <string>

W przypadku kompilatora MS VC ++ błąd występuje, ponieważ jeśli tego nie #include <string>zrobisz, nie operator<<zdefiniujesz dla std::string.

Kiedy kompilator próbuje skompilować ausgabe << f.getName();, szuka operator<<zdefiniowanego pliku std::string. Ponieważ nie zostało zdefiniowane, kompilator szuka alternatyw. Istnieje operator<<zdefiniowane for MyClassi kompilator próbuje go użyć, a aby go użyć, musi dokonać konwersji std::stringna MyClassi tak właśnie się dzieje, ponieważ MyClassma niejawny konstruktor! Tak więc kompilator kończy tworzenie nowej instancji Twojego MyClassi próbuje ponownie przesłać strumieniowo do strumienia wyjściowego. Powoduje to nieskończoną rekursję:

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

Aby uniknąć błędu, musisz #include <string>upewnić się, że operator<<zdefiniowano dla std::string. Powinieneś także MyClasswyraźnie określić konstruktor, aby uniknąć tego rodzaju nieoczekiwanej konwersji. Zasada mądrości: uczyń konstruktory jawnymi, jeśli przyjmują tylko jeden argument, aby uniknąć niejawnej konwersji:

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

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

To wygląda operator<<na std::stringdostanie zdefiniowane tylko wtedy, gdy <string>jest włączone (z kompilatora MS) iz tego powodu wszystko kompilacji, jednak można dostać nieco nieoczekiwane zachowanie jak operator<<jest uzyskiwanie nazwie rekurencyjnie dla MyClasszamiast dzwonić operator<<do std::string.

Czy to oznacza, że #include <iostream>string jest uwzględniany tylko częściowo?

Nie, ciąg jest w pełni uwzględniony, w przeciwnym razie nie byłbyś w stanie go użyć.

Pavel P.
źródło
19
@airborne - To nie jest „problem związany z Visual C ++”, ale co może się stać, jeśli nie dołączysz odpowiedniego nagłówka. Podczas korzystania std::stringbez #include<string>wszelkiego rodzaju rzeczy może się zdarzyć, nie ograniczając się do błędu czasu kompilacji. Wywołanie niewłaściwej funkcji lub niewłaściwego operatora to najwyraźniej inna opcja.
Bo Persson
15
Cóż, to nie jest „wywoływanie niewłaściwej funkcji lub niewłaściwego operatora”; kompilator robi dokładnie to, co mu kazałeś. Po prostu nie wiedziałeś, że kazałeś mu to zrobić;)
Lightness Races in Orbit
18
Użycie typu bez dołączania odpowiedniego pliku nagłówkowego jest błędem. Kropka. Czy implementacja mogła ułatwić wykrycie błędu? Pewnie. Ale to nie jest „problem” z implementacją, to problem z napisanym przez Ciebie kodem.
Cody Grey
4
Biblioteki standardowe mogą zawierać tokeny zdefiniowane w innym miejscu w std w sobie i nie muszą zawierać całego nagłówka, jeśli definiują jeden token.
Yakk - Adam Nevraumont
5
Dość zabawne jest to, że grupa programistów C ++ argumentuje, że kompilator i / lub biblioteka standardowa powinny włożyć więcej pracy, aby im pomóc. Wdrożenie jest tutaj zgodne z jego prawami, zgodnie ze standardem, jak wielokrotnie podkreślano. Czy można użyć „oszustwa”, aby uczynić to bardziej oczywistym dla programisty? Jasne, ale moglibyśmy również napisać kod w Javie i całkowicie uniknąć tego problemu. Dlaczego MSVC ma uwidaczniać swoich wewnętrznych pomocników? Dlaczego nagłówek miałby przeciągać kilka zależności, których w rzeczywistości nie potrzebuje? To narusza całego ducha języka!
Cody Grey
35

Problem polega na tym, że Twój kod wykonuje nieskończoną rekurencję. Operator przesyłania strumieniowego dla std::string( std::ostream& operator<<(std::ostream&, const std::string&)) jest zadeklarowany w <string>pliku nagłówkowym, chociaż std::stringsam jest zadeklarowany w innym pliku nagłówkowym (dołączonym przez oba <iostream>i <string>).

Jeśli nie <string>dołączasz kompilatora, kompilator próbuje znaleźć sposób na kompilację ausgabe << f.getName();.

Zdarza się, że zdefiniowałeś zarówno operator przesyłania strumieniowego dla, jak MyClassi konstruktor, który dopuszcza a std::string, więc kompilator używa go (poprzez niejawną konstrukcję ), tworząc wywołanie rekurencyjne.

Jeśli zadeklarujesz explicitswój konstruktor ( explicit MyClass(const std::string& s)), twój kod nie będzie już kompilowany, ponieważ nie ma możliwości wywołania operatora przesyłania strumieniowego za pomocą std::string, a będziesz zmuszony dołączyć <string>nagłówek.

EDYTOWAĆ

Moje środowisko testowe to VS 2010 i począwszy od poziomu ostrzeżenia 1 ( /W1) ostrzega Cię o problemie:

ostrzeżenie C4717: 'operator <<': rekurencja na wszystkich ścieżkach sterowania, funkcja spowoduje przepełnienie stosu w czasie wykonywania

cbuchart
źródło