Отладочные только потоки в С++?

Я реализовал вывод ostream для вывода отладки, который отправляет завершение отправки отладочной информации на OutputDebugString. Типичное его использование выглядит так (где debug - объект ostream):

debug << "some error\n";

Для версий релизов, какой наименее болезненный и наиболее эффективный способ не выводить эти отладочные заявления?

Ответ 1

Как насчет этого? Вы должны проверить, что на самом деле он ничего не оптимизирует в выпуске:

#ifdef NDEBUG
    class DebugStream {};
    template <typename T>
    DebugStream &operator<<(DebugStream &s, T) { return s; }
#else
    typedef ostream DebugStream;
#endif

Вам придется передать объект потока отладки как DebugStream &, а не как ostream &, поскольку в версиях сборки он не один. Это преимущество, так как если поток отладки не является ostream, это означает, что вы не несете обычное время выполнения пустого потока, поддерживающего интерфейс ostream (виртуальные функции, которые фактически вызываются, но ничего не делают).

Предупреждение: я только что сделал это, обычно я бы сделал что-то похожее на ответ Нила - имеет значение макроса, "только делайте это в сборке отладки", так что в источнике явно указано, что такое отладочный код, а что нет "т. Некоторые вещи, которые я действительно не хочу абстрагироваться.

Neil macro также обладает тем свойством, что он абсолютно определенно не оценивает свои аргументы в выпуске. Напротив, даже с моим шаблоном, вы обнаружите, что иногда:

debug << someFunction() << "\n";

не может быть оптимизирован ни к чему, потому что компилятор не обязательно знает, что someFunction() не имеет побочных эффектов. Конечно, если someFunction() имеет побочные эффекты, вы можете захотеть его вызвать в сборках релизов, но это своеобразное смешивание протоколирования и функционального кода.

Ответ 2

Самый распространенный (и, безусловно, самый эффективный) способ - удалить их с помощью препроцессора, используя что-то вроде этого (простейшая возможная реализация):

#ifdef RELEASE
  #define DBOUT( x )
#else
  #define DBOUT( x )  x
#endif

Затем вы можете сказать

DBOUT( debug << "some error\n" );

Изменить: Вы можете сделать DBOUT немного сложнее:

#define DBOUT( x ) \
   debug << x  << "\n"

который позволяет несколько более приятный синтаксис:

DBOUT( "Value is " << 42 );

Второй альтернативой является определение DBOUT как потока. Это означает, что вы должны реализовать какой-то класс нулевого потока - см. Внедрение no-op std:: ostream. Однако такой поток имеет служебные данные во время выполнения в сборке выпуска.

Ответ 3

Приятный метод:

#ifdef _DEBUG
#define DBOUT cout // or any other ostream
#else
#define DBOUT 0 && cout
#endif

DBOUT << "This is a debug build." << endl;
DBOUT << "Some result: " << doSomething() << endl;

Пока вы не делаете ничего странного, функции, вызываемые и передаваемые в DBOUT, не будут вызываться в сборках релизов. Этот макрос работает из-за приоритета оператора и логического И; потому что && имеет более низкий приоритет, чем <<, релиз строит компиляцию DBOUT << "a" как 0 && (cout << "a"). Логическое И не оценивает выражение справа, если выражение слева оценивается в 0 или false; потому что левое выражение всегда оценивается равным нулю, правое выражение всегда удаляется любым компилятором, который стоит использовать, за исключением случаев, когда вся оптимизация отключена (и даже тогда явно недостижимый код все равно может быть проигнорирован.)


Вот пример странных вещей, которые прервут этот макрос:

DBOUT << "This is a debug build." << endl, doSomething();

Следите за запятыми. doSomething() всегда будет вызываться, независимо от того, определена или нет _DEBUG. Это связано с тем, что оператор вычисляется в версиях релизов следующим образом:

(0 && (cout << "This is a debug build." << endl)), doSomething();
// evaluates further to:
false, doSomething();

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

DBOUT << "Value of b: " << (a, b) << endl;

Другой пример:

(DBOUT << "Hello, ") << "World" << endl; // Compiler error on release build

В версиях релизов это оценивается как:

(0 && (cout << "Hello, ")) << "World" << endl;
// evaluates further to:
false << "World" << endl;

который вызывает ошибку компилятора, потому что bool не может быть сдвинут слева указателем char, если не определен пользовательский оператор. Этот синтаксис также вызывает дополнительные проблемы:

(DBOUT << "Result: ") << doSomething() << endl;
// evaluates to:
false << doSomething() << endl;

Так же, как когда запятая была использована плохо, doSomething() все еще вызывается, потому что ее результат должен быть передан оператору сдвига влево. (Это может произойти только в том случае, если пользовательский оператор определен, который сдвигает слева от bool указателем char, в противном случае возникает ошибка компилятора.)

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

Ответ 4

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

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

С этой целью мои макросы выглядят как

#define TRACE_ERROR if (Debug::testLevel(Debug::Error)) DebugStream(Debug::Error)
#define TRACE_INFO  if (Debug::testLevel(Debug::Info))  DebugStream(Debug::Info)
#define TRACE_LOOP  if (Debug::testLevel(Debug::Loop))  DebugStream(Debug::Loop)
#define TRACE_FUNC  if (Debug::testLevel(Debug::Func))  DebugStream(Debug::Func)
#define TRACE_DEBUG if (Debug::testLevel(Debug::Debug)) DebugStream(Debug::Debug)

Хорошая вещь об использовании оператора if состоит в том, что для трассировки нет затрат на трассировку, которая не выводится, код трассировки только вызывается, если он будет напечатан.

Если вы не хотите, чтобы определенный уровень не отображался в сборках релизов, используйте константу, которая доступна во время компиляции в инструкции if.

#ifdef NDEBUG
    const bool Debug::DebugBuild = false;
#else
    const bool Debug::DebugBuild = true;
#endif

    #define TRACE_DEBUG if (Debug::DebugBuild && Debug::testLevel(Debug::Debug)) DebugStream(Debug::Debug)

Это сохраняет синтаксис iostream, но теперь компилятор оптимизирует выражение if из кода, в версиях сборки.

Ответ 5

#ifdef RELEASE
  #define DBOUT( x )
#else
  #define DBOUT( x )  x
#endif

Просто используйте это в самих операционных операциях. Вы даже можете написать для него один оператор.

template<typename T> Debugstream::operator<<(T&& t) {
    DBOUT(ostream << std::forward<T>(t);) // where ostream is the internal stream object or type
}

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

Я, конечно, использовал ссылки rvalue и совершенную пересылку, и нет никакой гарантии, что у вас есть такой компилятор. Но вы можете просто использовать const ref, если ваш компилятор совместим только с С++ 03.

Ответ 6

@iain: выходите из комнаты в поле для комментариев, поэтому отправляйте его здесь для ясности.

Использование утверждений if - неплохая идея! Я знаю, что если выражения в макросах могут иметь некоторые подводные камни, поэтому я должен быть особенно осторожным, строя их и используя их. Например:

if (error) TRACE_DEBUG << "error";
else do_something_for_success();

... завершит выполнение do_something_for_success(), если возникнет ошибка, и инструкции трассировки уровня отладки отключены, потому что оператор else связывается с внутренним оператором if. Однако в большинстве стилей кодирования используется использование фигурных скобок, которые решают проблему.

if (error) 
{
    TRACE_DEBUG << "error";
}
else
{
    do_something_for_success();
}

В этом фрагменте кода do_something_for_success() ошибочно выполняется, если трассировка на уровне отладки отключена.