Почему здесь #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.


person airborne    schedule 02.05.2017    source источник
comment
С милостью божьей. отлично работает с gcc, см. ideone.com/YCf4OI   -  person v78    schedule 02.05.2017
comment
Вы пробовали визуальную студию с Visual C ++ и закомментировали, включая ‹string›?   -  person airborne    schedule 02.05.2017
comment
@cbuchart: Хотя на этот вопрос уже был дан ответ, я думаю, что это достаточно сложная тема, поэтому иметь второй ответ другими словами полезно. Я проголосовал за восстановление вашего отличного ответа.   -  person Lightness Races in Orbit    schedule 02.05.2017
comment
Я должен согласиться, я не понимаю, почему, когда вы вызываете operator ‹< с помощью ostream & ausgabe, это приводит к бесконечной рекурсии.   -  person Chad    schedule 02.05.2017
comment
@ v78: В C ++ не указано, могут ли заголовки включать другие заголовки. Отсутствие заголовка может быть безвредным, если заголовок включен косвенно.   -  person MSalters    schedule 02.05.2017
comment
@MSalters, но им нельзя частично включать другие заголовки, не так ли?   -  person Ruslan    schedule 02.05.2017
comment
@Ruslan: Фактически, они есть. Другими словами, #include<iostream> и <string> могут включать <common/stringimpl.h>.   -  person MSalters    schedule 02.05.2017
comment
В 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   -  person CroCo    schedule 02.05.2017
comment
@CroCo то же самое в VS 2010, начиная с /W1   -  person cbuchart    schedule 03.05.2017


Ответы (2)


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

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

С компилятором MS VC ++ ошибка возникает, потому что, если вы не #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> строка включена только частично?

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

person Pavel P    schedule 02.05.2017
comment
@airborne, если это проблема, когда вы запускаете его в отладчике и просматриваете трассировку стека, она должна быть действительно большой и постоянно перемещаться между ними. - person xaxxon; 02.05.2017
comment
@xaxxon Скорее всего, вы этого не увидите, поскольку ctor закончит, и вы просто получите stacktrace, который выглядит так, как будто operator<< бесконечно вызывает сам себя. - person Pavel P; 02.05.2017
comment
@ Спасибо, Павел, ты прав! Когда я щелкаю правой кнопкой мыши - ›Перейти к определению в операторе ‹< в ausgabe ‹* f.getName (), я перехожу к строковому файлу, когда включаю строку, но когда нет, я просто перехожу на одну строку выше. Означает ли это, что через #include ‹iostream› строка включается только частично? И это конкретная проблема Visual C ++, потому что я просто попробовал ее с gcc, и она сработала - person airborne; 02.05.2017
comment
@airborne Нет, строка включена полностью, иначе вы не сможете ее использовать. operator<< просто отсутствует - person Pavel P; 02.05.2017
comment
@airborne - это не проблема Visual C ++, но что может случиться, если вы не добавите правильный заголовок. При использовании std::string без #include<string> могут происходить всевозможные вещи, не ограничиваясь ошибкой времени компиляции. Другой вариант - вызов неправильной функции или оператора. - person Bo Persson; 02.05.2017
comment
Что ж, это не вызов неправильной функции или оператора; компилятор делает именно то, что вы ему сказали. Вы просто не знали, что говорите ему об этом;) - person Lightness Races in Orbit; 02.05.2017
comment
@BoPersson Я бы назвал это проблемой VC ++, поскольку они должны были быть более внимательными, чтобы гарантировать, что std :: string не существует, если <string> не был включен. По сути, они могли бы включить внутри все, что захотят, но фактический строковый заголовок может вводить basic_string и все typedefs в пространство имен std (и без него std :: string не будет использоваться). - person Pavel P; 02.05.2017
comment
Использование типа без включения соответствующего файла заголовка является ошибкой. Период. Могла ли реализация упростить обнаружение ошибки? Конечно. Но это не проблема с реализацией, это проблема с кодом, который вы написали. - person Cody Gray; 02.05.2017
comment
Стандартные библиотеки могут включать токены, которые определены в другом месте в std внутри себя, и не обязаны включать весь заголовок, если они определяют один токен. - person Yakk - Adam Nevraumont; 02.05.2017
comment
@BoPersson Напротив, я бы сказал, убаюкивает неопытных программистов ложным чувством безопасности, предоставляя std::string, но ни один из помощников, указанных в <string>, в значительной степени не является проблемой MSVC, учитывая, что большинство компиляторов либо предоставляют все <string>, либо скрыть std::string, если включено <iostream>. - person Justin Time - Reinstate Monica; 02.05.2017
comment
Проблема не в том, чтобы <iostream> сделать std::string видимым (что, если говорить технически, может рассматриваться как косвенно требуемое по стандарту, поскольку istream и ostream косвенно происходят от ios_base, который определяет тип члена, ios_base::failure, у которого есть конструктор, который принимает const string& (как указано в [ios::failure])), но в MSVC из-за невозможности сделать видимыми соответствующие помощники (хотя им разрешено воздерживаться от этого, на самом деле они не должны этого делать). - person Justin Time - Reinstate Monica; 02.05.2017
comment
Я лично считаю, что им следовало либо использовать некоторые уловки компилятора, чтобы разрешить ios_base::failure (вместе с любыми другими членами, которые могут этого требовать) использовать std::string, не делая определение внешне видимым, либо просто включить <string> напрямую, вместо того, чтобы допускать подобные ошибки. - person Justin Time - Reinstate Monica; 02.05.2017
comment
По крайней мере, они должны были быть достаточно вежливыми, чтобы заголовок выдал предупреждение, если std::string используется пользователем, когда <iostream> включен, а <string> нет, но это потребует больше усилий с их стороны; они могли <iostream> включить отключенное по умолчанию предупреждение уровня 1, если <string> защита заголовка не определена (вероятно, в самом конце заголовка), и <string> отключить это предупреждение, если оно было включено. [Это предупреждение должно оставаться отключенным, если оно неприменимо, даже если передано /Wall, что потребует дополнительной работы с их стороны.] - person Justin Time - Reinstate Monica; 02.05.2017
comment
@CodyGray Честно говоря, это понятная ошибка (<iostream> косвенно требует std::string, как упоминалось в моем третьем комментарии выше этого, поэтому им придется изо всех сил не предоставлять std::string, когда <iostream> включен), и проблема заключается в том, что реализация делает ровно достаточно, чтобы соблазнить новичков ложным чувством безопасности, не делая ничего, чтобы фактически сообщить им об их ошибке. - person Justin Time - Reinstate Monica; 02.05.2017
comment
@Pavel, Я бы назвал эту проблему VC ++, так как они должны были быть более внимательными, чтобы гарантировать, что std :: string не существует Я думаю, они сделали это, предоставив это предупреждение warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflow - person CroCo; 02.05.2017
comment
@CroCo Не думаю, что они это сделали. В этом случае просто случается, что код приводит к бесконечной рекурсии и выдает предупреждение. Проблема в том, что непреднамеренный код запускается без заголовка. Настоящая ошибка IMO заключается в том, что ctor не является явным и заставляет компилятор делать то, что не предназначено. - person Pavel P; 02.05.2017
comment
В конечном итоге вина лежит на пользователе, но ошибка может существовать именно из-за MS VC ++. То, что они могут оставлять вещи полуопределенными, не означает, что они должны. - person n.caillou; 03.05.2017
comment
Довольно забавно видеть, как группа программистов на C ++ утверждает, что компилятор и / или стандартная библиотека должны делать больше, чтобы помочь им. В соответствии со стандартом, реализация здесь находится в пределах своих прав, как уже неоднократно указывалось. Можно ли использовать хитрости, чтобы сделать это более очевидным для программиста? Конечно, но мы могли бы написать код на Java и вообще избежать этой проблемы. Почему MSVC должен делать свои внутренние помощники видимыми? Зачем в заголовок тащить кучу зависимостей, которые ему на самом деле не нужны? Это нарушает весь дух языка! - person Cody Gray; 03.05.2017
comment
@CodyGray Не говоря уже о том, что, скажем, потоки в стандартной библиотеке разделены примерно на 20 разных заголовков, предположительно, чтобы помочь с временем компиляции и размерами исполняемых файлов, и, похоже, это никого не беспокоит. - person Joker_vD; 03.05.2017
comment
@CodyGray Правда. Мне просто не нравится, что их реализация в основном ставит ловушку для новичков, для меня это немного похоже на злонамеренное соблюдение. - person Justin Time - Reinstate Monica; 03.05.2017
comment
@JustinTime C ++ - гигантская ловушка для новичков. Если вам это не нравится, используйте другой язык. - person Hong Ooi; 04.05.2017
comment
@HongOoi Мне это не нравится, но я могу счесть необходимым работать на C ++ по любому количеству причин (это язык, используемый на моем рабочем месте, или есть библиотека, которую мне нужно использовать только на C ++, и т. Д.). Что еще более важно, в конечном итоге все (не только разработчики!) Страдают от программного обеспечения с ошибками и инструментов, которые упрощают написание ошибок и затрудняют их предотвращение, делают программное обеспечение с ошибками более распространенным. - person Kyle Strand; 10.05.2017

Проблема в том, что ваш код выполняет бесконечную рекурсию. Оператор потоковой передачи для 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: 'operator ‹<': рекурсивно на всех путях управления, функция вызовет переполнение стека выполнения

person cbuchart    schedule 02.05.2017