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

121

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

#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.

бортовой
источник
4
С Божьей милостью. отлично работает на gcc, см. ideone.com/YCf4OI
v78
вы пробовали визуальную студию с Visual C ++ и закомментировали include <string>?
поднялся в воздух
1
@cbuchart: Хотя на этот вопрос уже был дан ответ, я думаю, что это достаточно сложная тема, поэтому иметь второй ответ другими словами полезно. Я проголосовал за восстановление вашего отличного ответа.
Гонки
5
@Ruslan: Фактически, они есть. Так сказать, #include<iostream>и <string>оба могут включать <common/stringimpl.h>.
MSalters
3
В Visual Studio 2015 вы получаете предупреждение ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflowпри запуске этой строкиcl /EHsc main.cpp /Fetest.exe
CroCo

Ответы:

161

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

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

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

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

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

Чтобы избежать ошибки, вам необходимо #include <string>убедиться, что operator<<для std::string. Также вы должны сделать свой 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>строка включена только частично?

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

Павел П.
источник
19
@airborne - это не «специфическая проблема Visual C ++», но то, что может случиться, если вы не добавите правильный заголовок. При использовании std::stringбез него #include<string>могут произойти все, не ограничиваясь ошибкой времени компиляции. Другой вариант - вызов неправильной функции или оператора.
Bo Persson
15
Что ж, это не «вызов неправильной функции или оператора»; компилятор делает именно то, что вы ему сказали. Вы просто не знали, что говорите ему об этом;)
Гонки
18
Использование типа без включения соответствующего файла заголовка является ошибкой. Период. Могла ли реализация упростить обнаружение ошибки? Конечно. Но это не «проблема» реализации, это проблема написанного вами кода.
Коди Грей
4
Стандартные библиотеки могут включать в себя токены, которые определены где-то в std внутри себя, и не обязаны включать весь заголовок, если они определяют один токен.
Якк - Адам Неврамонт
5
Довольно забавно видеть, как группа программистов на C ++ утверждает, что компилятор и / или стандартная библиотека должны делать больше, чтобы помочь им. В соответствии со стандартом реализация находится в пределах своих прав, о чем неоднократно указывалось. Можно ли использовать «обман», чтобы сделать это более очевидным для программиста? Конечно, но мы могли бы написать код на Java и полностью избежать этой проблемы. Почему MSVC должен делать свои внутренние помощники видимыми? Зачем в заголовок тащить кучу зависимостей, которые ему на самом деле не нужны? Это нарушает весь дух языка!
Коди Грей
35

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

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

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

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

РЕДАКТИРОВАТЬ

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

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

cbuchart
источник