Verbesserung der Druckprotokollierung mit Line Pos Info &Modern C++

Verbesserung der Druckprotokollierung mit Line Pos Info &Modern C++

Egal wie gut Sie sind, ich denke, Sie könnten immer noch eine der primären Methoden zum Debuggen verwenden:Trace-Werte mit printf , TRACE , outputDebugString , etc… und scannen Sie dann die Ausgabe während des Debuggens.

Das Hinzufügen von Informationen über die Zeilennummer und die Datei, aus der die Protokollnachricht stammt, ist eine sehr effiziente Methode, die Ihnen viel Zeit sparen kann. In diesem Beitrag beschreibe ich einen Trick, der in Visual Studio besonders nützlich ist, aber auch in anderen IDEs/Compilern hilfreich sein könnte.

Ich zeige Ihnen auch, wie modernes C++ und C++20 Code schöner machen.

Der Trick

Beim Debuggen von C++-Code ist es sehr praktisch, Werte an die Konsole oder das Ausgabefenster auszugeben und das Protokoll zu scannen. So einfach wie:

std::cout << "my val: " << val << '\n';

Sie können diese Technik leicht verbessern, indem Sie LINE- und FILE-Informationen hinzufügen. Auf diese Weise sehen Sie die Quelle dieser Nachricht. Dies kann sehr praktisch sein, wenn Sie viele Protokolle scannen.

In Visual Studio gibt es einen Trick, mit dem Sie schnell vom Debug-Ausgabefenster zu einer bestimmten Codezeile wechseln können.

Sie müssen lediglich das folgende Format verwenden:

"%s(%d): %s", file, line, message

Zum Beispiel:

myfile.cpp(32) : Hello World

Sie können jetzt auf die Zeile im VS-Ausgabefenster doppelklicken, und VS öffnet sofort myfile.cpp in Zeile 32. Siehe unten für eine Datei namens DebuggingTipsSamples.cpp :

Warum ist es so wichtig? In meinem Fall habe ich viel Zeit verloren, als ich versucht habe, nach dem Ursprung einiger Protokollausgaben zu suchen. Wenn ich eine Nachricht sehe, kopiere ich sie, suche nach der Lösung, und normalerweise komme ich nach dem Scrollen endlich zur richtigen Codezeile. Dieser Doppelklick-Ansatz ist unschlagbar, da er viel effizienter ist!

Nun, da Sie das richtige Format der Nachricht kennen, wie verwenden Sie es im Code? Gehen wir Schritt für Schritt vor.

Wir implementieren diesen Code mit „Standard“-C++, wechseln dann zu modernem C++ und sehen schließlich, was mit C++20 kommt.

Standard-C++ für Visual Studio und Windows

Für VS müssen Sie zunächst die Nachricht mit OutputDebugString ausgeben (Gewinnspezifische Funktion):

OutputDebugString("myfile.cpp(32) : super");

Zweitens ist es besser, die obige Funktion mit einem Trace/Log-Makro zu umschließen:

#define MY_TRACE(msg, ...) \
    MyTrace(__LINE__, __FILE__, msg, __VA_ARGS__)

Sie können es folgendermaßen verwenden:

MY_TRACE("hello world %d", myVar);

Der obige Code ruft MyTrace auf Funktion, die intern OutputDebugString aufruft .

Warum ein Makro? Es dient der Bequemlichkeit. Andernfalls müssten wir die Zeilennummer und den Dateinamen manuell übergeben. Datei und Zeile können nicht innerhalb von MyTrace abgerufen werden weil es immer auf den Quellcode zeigen würde, wo MyTrace implementiert ist – nicht der Code, der es aufruft.

Was sind __FILE__ und __LINE__ ? In Visual Studio (siehe msdn) sind dies vordefinierte Makros, die in Ihrem Code verwendet werden können. Wie der Name schon sagt, erweitern sie sich in den Dateinamen des Quellcodes und die genaue Zeile in einer bestimmten Übersetzungseinheit. Um die __FILE__ zu steuern Makro können Sie die Compiler-Option /FC verwenden . Die Option macht Dateinamen länger (vollständiger Pfad) oder kürzer (relativ zum Lösungsverzeichnis). Bitte beachten Sie, dass /FC wird impliziert, wenn Bearbeiten und Fortfahren verwendet wird.

Bitte beachten Sie, dass __FILE__ und __LINE__ sind ebenfalls vom Standard vorgegeben, daher sollten andere Compiler sie ebenfalls implementieren. Siehe in 19.8 Vordefinierte Makronamen .

Gleiches gilt für __VA_ARGS__ :siehe 19.3 Makro-Ersetzung - cpp.replace

Und hier ist die Implementierung von MyTrace :

void MyTrace(int line, const char *fileName, const char *msg, ...) {
    va_list args;
    char buffer[256] = { 0 };
    sprintf_s(buffer, sizeof(buffer), "%s(%d) : ", fileName, line);
    OutputDebugString(buffer);

    // retrieve the variable arguments
    va_start(args, msg);
    vsprintf_s(buffer, msg, args);
    OutputDebugString(buffer);
    va_end(args);
}

Aber Makros sind nicht schön… wir haben auch diese C-Stil va_start Methoden… können wir stattdessen etwas anderes verwenden?

Mal sehen, wie wir hier modernes C++ verwenden können

Variadic-Vorlagen zur Rettung!

MyTrace unterstützt eine variable Anzahl von Argumenten ... aber wir verwenden va_start /va_end Technik, die die Argumente zur Laufzeit scannt … aber wie sieht es mit der Kompilierzeit aus?

In C++17 können wir den Fold-Ausdruck nutzen und den folgenden Code verwenden:

#define MY_TRACE_TMP(...) MyTraceImplTmp(__LINE__, __FILE__, __VA_ARGS__)

template <typename ...Args>
void MyTraceImplTmp(int line, const char* fileName, Args&& ...args) {
    std::ostringstream stream;
    stream << fileName << "(" << line << ") : ";
    (stream << ... << std::forward<Args>(args)) << '\n';

    OutputDebugString(stream.str().c_str());
}

// use like:
MY_TRACE_TMP("hello world! ", 10, ", ", 42);

Der obige Code nimmt eine variable Anzahl von Argumenten und verwendet ostringstream um eine einzelne Saite zu bauen. Dann geht die Zeichenfolge zu OutputDebugString .

Dies ist nur eine grundlegende Implementierung und vielleicht nicht perfekt. Wenn Sie möchten, können Sie mit dem Protokollierungsstil experimentieren und es sogar mit einem Ansatz zur vollständigen Kompilierzeit versuchen.

Es gibt auch andere Bibliotheken, die hier helfen könnten:zum Beispiel {fmt} oder pprint - von J. Galowicz.

C++20 und keine Makros?

Während des letzten ISO-Meetings akzeptierte das Komitee std::source_location in C++20!

C++ Extensions for Library Fundamentals, Version 2 – 14.1 Klasse source_location

Dieser neue Bibliothekstyp wird wie folgt deklariert:

struct source_location {
    static constexpr source_location current() noexcept;
    constexpr source_location() noexcept;
    constexpr uint_least32_t line() const noexcept;
    constexpr uint_least32_t column() const noexcept;
    constexpr const char* file_name() const noexcept;
    constexpr const char* function_name() const noexcept;
};

Und hier ist ein einfaches Beispiel, angepasst von cppreference/source_location:

#include <iostream>
#include <string_view>
#include <experimental/source_location>

using namespace std;
using namespace std::experimental; 

void log(const string_view& message, 
      const source_location& location = source_location::current()) {
    std::cout << "info:"
              << location.file_name() << ":"
              << location.line() << " "
              << location.function_name() << " "
              << message << '\n';         
}

int main() {
    log("Hello world!");

    // another log
    log("super extra!");
}

Wir können das Beispiel umschreiben oder einloggen in

template <typename ...Args>
void TraceLoc(const source_location& location, Args&& ...args) {
    std::ostringstream stream;
    stream << location.file_name() << "(" << location.line() << ") : ";
    (stream << ... << std::forward<Args>(args)) << '\n';

    std::cout << stream.str();
}

Spielen Sie mit dem Code @Coliru

(Stand März 2021, source_location ist in VS 2019 16.10 und GCC 11 verfügbar)

Jetzt, anstatt __FILE__ zu verwenden und __LINE__ Wir haben ein Standardbibliotheksobjekt, das alle nützlichen Informationen enthält.

Wir können auch einige std::format werfen :

template <typename ...Args>
void TraceLoc(const std::source_location& loc, Args&& ...args) {
	auto str = fmt::format("{}({}): {}\n", loc.file_name(), loc.line(), 
                            fmt::format(std::forward<Args>(args)...));

	std::cout << str;
}

TraceLoc(std::source_location::current(), "{}, {}, {}", "hello world", 10, 42);

(Bitte ersetzen Sie fmt:: mit std:: einmal std::format ist in unseren Compilern verfügbar :))

Und spielen Sie mit dem Code @Compiler Explorer

Leider können wir das Source-Location-Argument nicht nach variadischen Argumenten verschieben… also müssen wir immer noch Makros verwenden, um es zu verbergen.

Weißt du, wie man es repariert? damit wir am Ende ein Standardargument verwenden können?

Idealerweise:

template <typename ...Args>
void TraceLoc(Args&& ...args, 
              const source_location& location = source_location::current())
{
   // ...
}

Wir können auf nicht-terminale variadische Template-Parameter im @cor3ntin-Blog warten (hoffentlich wird diese Funktion für C++23 in Betracht gezogen).

Es gibt auch eine Technik, die wir verwenden können, vorgeschlagen von einem Kommentar, wo wir ein Logger-Objekt mit einem Konstruktor verwenden können, der den Quellort nimmt … Ich werde diesen Trick das nächste Mal zeigen.

Zusätzliche Tools

In Visual Studio ist es auch möglich, Ablaufverfolgungspunkte zu verwenden (Danke xtofl für die Erwähnung in einem Kommentar!).

Wenn Sie einen Haltepunkt setzen, können Sie ihn grundsätzlich auswählen und „Aktionen“ auswählen und einen Ausdruck schreiben, der ausgewertet und an das Debugger-Ausgabefenster gesendet wird. Stellen Sie sicher, dass „Ausführung fortsetzen“ eingestellt ist. Diese Technik kann praktisch sein, wenn Sie große Sammlungen durchlaufen und nicht jede Iteration manuell schrittweise ausführen möchten. Ein Nachteil ist, dass es die Anwendung verlangsamen kann, da es nicht direkt aus dem Code aufgerufen wird.

Sehen Sie sich einen Screenshot einer einfachen Debugging-Sitzung an:

Und weitere Informationen:

  • Protokollinformationen mit Ablaufverfolgungspunkten – Visual Studio | Microsoft Docs
  • TracePoint :Eine tolle Funktion von Visual Studio | Code Wala

Eine ähnliche Funktion ist auch in GDB verfügbar – Tracepoints (Debugging with GDB)

Zusammenfassung

In diesem Artikel habe ich eine nützliche Technik gezeigt, die das Debuggen und Protokollieren im einfachen printf-Stil verbessern könnte.

Anfangs haben wir einen populären Code genommen, der hauptsächlich im C-Stil ist, und dann versuchten wir, ihn mit modernem C++ zu aktualisieren. Das erste war, variadische Template-Argumente zu verwenden. Auf diese Weise können wir die Eingabeparameter zur Kompilierzeit scannen, anstatt va_start zu verwenden /va_end C-Laufzeitfunktionen. Der nächste Schritt bestand darin, die zukünftige Implementierung von source_location zu betrachten ein neuer Typ, der in C++20 kommen wird.

Mit source_location wir könnten die Verwendung von __FILE__ überspringen und __LINE__ vordefinierte Makros, aber immer noch das Protokollierungsmakro (#define LOG(...) ) ist hilfreich, da es einen Standardparameter mit den Standortinformationen ausblenden kann.

Code aus dem Artikel:@github.

Wie sieht es mit Ihrem Compiler/IDE aus? Verwenden Sie auch eine solche Line/Pos-Funktionalität? Vielleicht enthält Ihre Logging-Bibliothek bereits solche Verbesserungen?