Как узнать точную строку кода, где было вызвано исключение?

Если я сам создаю исключение, я могу включить в него любую информацию: номер строки кода и имя исходного файла. Что-то вроде этого:

throw std::exception("myFile.cpp:255");

Но что с необработанными исключениями или с исключениями, сгенерированными не мной?

9 ответов

Решение

Кажется, что все пытаются улучшить ваш код, чтобы исключить его, и никто не пытается ответить на фактически заданный вами вопрос.

Это потому, что это невозможно сделать. Если код, который вызывает исключение, представлен только в двоичном виде (например, в файле LIB или DLL), то номер строки пропал, и нет способа соединить объект с линией в исходном коде.

Лучшее решение - использовать пользовательский класс и макрос.:-)

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>

class my_exception : public std::runtime_error {
    std::string msg;
public:
    my_exception(const std::string &arg, const char *file, int line) :
    std::runtime_error(arg) {
        std::ostringstream o;
        o << file << ":" << line << ": " << arg;
        msg = o.str();
    }
    ~my_exception() throw() {}
    const char *what() const throw() {
        return msg.c_str();
    }
};
#define throw_line(arg) throw my_exception(arg, __FILE__, __LINE__);

void f() {
    throw_line("Oh no!");
}

int main() {
    try {
        f();
    }
    catch (const std::runtime_error &ex) {
        std::cout << ex.what() << std::endl;
    }
}

Есть несколько возможностей узнать, где было сгенерировано исключение:

Использование макросов компилятора

С помощью __FILE__ а также __LINE__ макросы в месте выброса (как уже показано другими комментаторами), либо используя их в исключениях std в виде текста, либо в качестве отдельных аргументов для пользовательского исключения:

Либо использовать

throw std::runtime_error(msg " at " `__FILE__` ":" `__LINE__`);

или бросить

class my_custom_exception {
  my_custom_exception(const char* msg, const char* file, unsigned int line)
...

Обратите внимание, что даже при компиляции для Unicode (в Visual Studio) FILE расширяется до однобайтовой строки. Это работает в отладке и выпуске. К сожалению, имена исходных файлов с исключениями для бросания кода помещаются в выходной исполняемый файл.

Ходьба стека

Выясните местоположение исключения, пройдя по стеку вызовов.

  • В Linux с gcc функции backtrace() и backtrace_symbols() могут получить информацию о текущем стеке вызовов. Смотрите документацию gcc как их использовать. Код должен быть скомпилирован с -g, чтобы символы отладки помещались в исполняемый файл.

  • В Windows вы можете пройтись по стеку, используя библиотеку dbghelp и ее функцию StackWalk64. Подробности смотрите в статье Йохена Калмбаха о CodeProject. Это работает в отладке и выпуске, и вам нужно отправить файлы.pdb для всех модулей, о которых вы хотите получить информацию.

Вы даже можете объединить два решения, собирая информацию о стеке вызовов при возникновении пользовательского исключения. Стек вызовов может храниться в исключении, как в.NET или Java. Обратите внимание, что сбор стека вызовов в Win32 очень медленный (мой последний тест показал около 6 собранных стеков вызовов в секунду). Если ваш код вызывает много исключений, этот подход значительно замедляет вашу программу.

Самое простое решение - использовать макрос:

#define throw_line(msg) \
    throw std::exception(msg " " __FILE__ ":" __LINE__)

void f() {
    throw_line("Oh no!");
}

Пока никто не упомянул о повышении. Если вы используете библиотеки boost C++, для этого есть несколько хороших значений по умолчанию:

#include <boost/exception/diagnostic_information.hpp>
#include <exception>
#include <iostream>

struct MyException : std::exception {};

int main()
{
  try
  {
    BOOST_THROW_EXCEPTION(MyException());
  }
  catch (MyException &ex)
  {
    std::cerr << "Unexpected exception, diagnostic information follows:\n"
              << boost::current_exception_diagnostic_information();
  }
  return 0;
}

И тогда вы можете получить что-то вроде:

Unexpected exception, diagnostic information follows:
main.cpp(10): Throw in function int main()
Dynamic exception type: boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<MyException> >
std::exception::what: std::exception

Документы: https://www.boost.org/doc/libs/1_63_0/libs/exception/doc/diagnostic_information.html

Если у вас есть отладочная сборка и вы запускаете ее в отладчике Visual Studio, то вы можете взломать отладчик при возникновении любого исключения, прежде чем он распространится на весь мир.

Включите это с помощью альтернативы меню Debug > Exceptions, а затем отметьте типы исключений, которые вас интересуют.

Вы также можете добавить возможность создания файла дампа, если исходный код приложения принадлежит вам. С файлом дампа и файлами PDB (символами) для конкретной сборки вы получите, например, трассировки стека с помощью WinDbg.

Вдохновленный ответом Фрэнка Крюгера и документацией для std::nested_exception, я понял, что вы можете объединить ответ Фрэнка, который я использовал некоторое время, с std::nested_exception для создания полной трассировки стека ошибок с информацией о файлах и строках, Например, с моей реализацией, работает

#include "Thrower.h"
#include <iostream>
// runs the sample function above and prints the caught exception
int main ( )
{
    try {
        // [Doing important stuff...]
        try {
            std::string s = "Hello, world!";
            try {
                int i = std::stoi ( s );
            }
            catch ( ... ) {
                thrower ( "Failed to convert string \"" + s + "\" to an integer!" );
            }
        }
        catch ( Error& e ) {
            thrower ( "Failed to [Do important stuff]!" );
        }
    }
    catch ( Error& e ) {
        std::cout << Error::getErrorStack ( e );
    }
    std::cin.get ( );
}

выходы

ERROR: Failed to [Do important stuff]!
@ Location:c:\path\main.cpp; line 33
 ERROR: Failed to convert string "Hello, world!" to an integer!
 @ Location:c:\path\main.cpp; line 28
  ERROR: invalid stoi argument

Вот моя реализация:

#include <sstream>
#include <stdexcept>
#include <regex>

class Error : public std::runtime_error
{
    public:
    Error ( const std::string &arg, const char *file, int line ) : std::runtime_error( arg )
    {
        loc = std::string ( file ) + "; line " + std::to_string ( line );
        std::ostringstream out;
        out << arg << "\n@ Location:" << loc;
        msg = out.str( );
        bareMsg = arg;      
    }
    ~Error( ) throw() {}

    const char * what( ) const throw()
    {
        return msg.c_str( );
    }
    std::string whatBare( ) const throw()
    {
        return bareMsg;
    }
    std::string whatLoc ( ) const throw( )
    {
        return loc;
    }
    static std::string getErrorStack ( const std::exception& e, unsigned int level = 0)
    {
        std::string msg = "ERROR: " + std::string(e.what ( ));
        std::regex r ( "\n" );
        msg = std::regex_replace ( msg, r, "\n"+std::string ( level, ' ' ) );
        std::string stackMsg = std::string ( level, ' ' ) + msg + "\n";
        try
        {
            std::rethrow_if_nested ( e );
        }
        catch ( const std::exception& e )
        {
            stackMsg += getErrorStack ( e, level + 1 );
        }
        return stackMsg;
    }
    private:
        std::string msg;
        std::string bareMsg;
        std::string loc;
};

// (Important modification here)
// the following gives any throw call file and line information.
// throw_with_nested makes it possible to chain thrower calls and get a full error stack traceback
#define thrower(arg) std::throw_with_nested( Error(arg, __FILE__, __LINE__) )

`` `

Я думаю, что трассировка стека должна привести вас к сути.

Я нашел 2 решения, но ни одно из них не является полностью удовлетворительным:

  1. Если вы позвоните std::set_terminateВы можете оттуда распечатать стек вызовов прямо из стороннего исключения. К сожалению, нет способа восстановиться из обработчика завершения, и, следовательно, ваше приложение умрет.

  2. Если вы позвоните std::set_unexpectedтогда вам нужно объявить как можно больше из ваших функций с помощью throw(MyControlledException), так что когда они выдают из-за сторонних вызываемых функций, ваш unexpected_handler сможет дать вам детальное представление о том, куда бросило ваше приложение.

Скомпилируйте свое программное обеспечение в режиме отладки и запустите его с помощью valgrind. Это в основном для поиска утечек памяти, но также может показать вам точную строку, где происходят исключения.valgrind --leak-check=full /path/to/your/software.

Другие уже предложили использовать макрос и, возможно, собственный класс. Но если у вас есть иерархия исключений, вам также необходимо указать тип исключения при генерации:

#define THROW(ExceptionType, message)                                    \
    throw ExceptionType(std::string(message) + " in " + __FILE__ + ':'   \
                        + std::to_string(__LINE__) + ':' + __func__)

THROW(Error, "An error occurred");

Здесь предполагается, что все исключения принимают единственный строковый аргумент.

Помимо использования собственного класса с макросом, как предложено Фрэнком Крюгером, для ваших собственных исключений, вам может быть интересно взглянуть на механизм структурированной обработки исключений (вы программируете под окнами, верно?)
Проверьте структурированную обработку исключений на MSDN

Другие вопросы по тегам