Почему здесь #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 <<': рекурсивно на всех путях управления, функция вызовет переполнение стека выполнения

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.