Почему #include <string> предотвращает ошибку здесь?

Это мой пример кода:

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

Если я прокомментирую #include <string>, я не получаю никакой ошибки компилятора, я думаю, потому что это как-то включается через #include <iostream>. Если я "щелкните правой кнопкой мыши → Перейти к определению" в Microsoft VS, они оба указывают на ту же строку в файле xstring:

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

Но когда я запускаю свою программу, я получаю ошибку исключения:

0x77846B6E (ntdll.dll) в OperatorString.exe: 0xC00000FD: переполнение стека (параметр: 0x00000001, 0x01202FC4)

Любая идея, почему я получаю ошибку времени выполнения при комментировании #include <string>? Я использую VS 2013 Express.

Ответ 1

Действительно, очень интересное поведение.

Любая идея, почему я получаю ошибку времени выполнения при комментировании #include <string>

С компилятором MS VС++ ошибка возникает, потому что если вы не #include <string>, у вас не будет operator<<, определенного для std::string.

Когда компилятор пытается скомпилировать ausgabe << f.getName();, он ищет operator<<, определенный для std::string. Поскольку он не был определен, компилятор ищет альтернативы. Для MyClass существует operator<<, и компилятор пытается его использовать, и для его использования он должен преобразовать std::string в MyClass, и это именно то, что происходит, потому что MyClass имеет неявный конструктор! Итак, компилятор заканчивает создание нового экземпляра вашего MyClass и пытается передать его снова в выходной поток. Это приводит к бесконечной рекурсии:

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

Чтобы избежать ошибки, вам нужно #include <string> убедиться, что для std::string существует operator<<. Также вы должны сделать свой конструктор MyClass явным, чтобы избежать такого неожиданного преобразования. Правило мудрости: сделать конструкторы явными, если они принимают только один аргумент, чтобы избежать неявного преобразования:

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

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

Похоже, что operator<< для std::string определяется только тогда, когда <string> включен (с компилятором MS), и по этой причине все компилируется, однако вы получаете несколько неожиданное поведение, поскольку operator<< получает вызов рекурсивно для MyClass вместо вызова operator<< для std::string.

Означает ли это, что строка #include <iostream> включена только частично?

Нет, строка полностью включена, иначе вы не сможете ее использовать.

Ответ 2

Проблема заключается в том, что ваш код выполняет бесконечную рекурсию. Оператор потоковой передачи для std::string (std::ostream& operator<<(std::ostream&, const std::string&)) объявлен в заголовочном файле <string>, хотя сам std::string объявлен в другом файле заголовка (включенном как <iostream>, так и <string>).

Когда вы не включаете <string>, компилятор пытается найти способ скомпилировать ausgabe << f.getName();.

Бывает, что вы определили как оператор потоковой передачи для MyClass, так и конструктор, который допускает std::string, поэтому компилятор использует его (через неявное построение), создавая рекурсивный вызов.

Если вы объявите explicit ваш конструктор (explicit MyClass(const std::string& s)), ваш код больше не будет компилироваться, так как нет способа вызвать оператора потоковой передачи с std::string, и вы будете вынуждены включить <string>.

ИЗМЕНИТЬ

Моя тестовая среда - VS 2010, и начиная с уровня предупреждения 1 (/W1) он предупреждает вас о проблеме:

предупреждение C4717: 'оператор < <: рекурсивный на всех путях управления, функция вызовет переполнение стека выполнения