C++26: de nieuwe debugging-header

#if defined(_MSC_VER)
__debugbreak();
#elif defined(__clang__)
__builtin_debugtrap();
#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
__asm__ volatile("int3");
#else
raise(SIGTRAP);
#endif

Vrijwel elk groot C++-project vindt het wiel op deze manier opnieuw uit. Boost.Test heeft debuggerbreak(), Catch2 heeft CATCHBREAKINTODEBUGGER, Dear ImGui heeft IMDEBUGBREAK() en Unreal heeft UEDEBUGBREAK. De benodigde platformkennis is obscuur, maar bekend bij implementatie-experts — het was simpelweg niet gestandaardiseerd.

C++26 lost dit op met P2546R5 en het begeleidende P2810R4, dank aan Rene Ferdinand Rivera Morell. Een nieuwe <debugging> header biedt drie functies.

De drie functies

#include <debugging>

namespace std {
    void breakpoint() noexcept;
    bool is_debugger_present() noexcept;
    void breakpoint_if_debugging() noexcept;
}

Alle drie de functies zijn noexcept en freestanding. Geen van hen is constexpr, aangezien debugging een runtime-aangelegenheid is.

std::breakpoint()

Dit is een onvoorwaardelijk breakpoint. Wanneer er een debugger is gekoppeld, stopt de uitvoering en wordt de controle overgedragen aan de debugger. Op x86 compileert dit doorgaans naar een enkele int3-instructie.

Wanneer er geen debugger aanwezig is, is het gedrag implementatie-afhankelijk. Op de meeste platforms ontvangt het proces een signaal (zoals SIGTRAP) en zal het beëindigen, tenzij het signaal wordt opgevangen.

std::isdebuggerpresent()

Deze functie geeft true terug als het programma momenteel wordt gevolgd door een debugger. De controle wordt bij elke aanroep direct uitgevoerd; het resultaat wordt niet gecachet, omdat een debugger op elk gewenst moment tijdens de uitvoering kan worden gekoppeld of losgekoppeld.

Hoe dit onder de motorkap werkt, verschilt per platform. Op Windows kan een implementatie bijvoorbeeld ::IsDebuggerPresent() aanroepen. Op Linux leest de libstdc++ van GCC het bestand /proc/self/status en controleert het veld TracerPid. De standaard schrijft geen specifiek mechanisme voor — de semantiek is implementatie-afhankelijk, waarbij het beoogde gedrag wordt beschreven in niet-normatieve notities.

Wat deze functie interessant maakt, is dat hij vervangbaar is, in dezelfde zin als operator new en operator delete. Je kunt je eigen versie definiëren:

bool std::is_debugger_present() noexcept {
    return my_hardware_debug_flag;
}

Dit is geïntroduceerd door P2810R4 om het probleem met freestanding omgevingen op te lossen: op bare-metal platforms is er mogelijk geen manier op OS-niveau om een debugger te detecteren. Zonder vervangbaarheid zou de functie ofwel uitgesloten moeten worden van freestanding (wat overal #ifdef-guards zou vereisen) of een nutteloze standaardwaarde zouden moeten teruggeven. Vervangbaarheid stelt je in staat om de functie te koppelen aan wat jouw platform ook biedt.

std::breakpointifdebugging()

Dit is de gemaksfunctie waar de meeste gebruikers voor zullen kiezen. Het is equivalent aan:

if (std::is_debugger_present()) {
    std::breakpoint();
}

Wanneer er een debugger is gekoppeld, raak je een breakpoint. Als dat niet zo is, gebeurt er niets. Dit is het patroon dat Catch2, Boost.Test en elk ander framework handmatig implementeert.

Meer dan breakpoints

Het feit dat isdebuggerpresent() een aparte functie is, maakt meer mogelijk dan alleen conditionele breakpoints. Je zou het kunnen gebruiken om:

  • Extra diagnostische output te genereren tijdens het debuggen.
  • Time-outs uit te schakelen die zouden afgaan tijdens een stepping-sessie.
  • Over te schakelen naar een allocator die vriendelijker is voor debuggers.
  • Optimalisaties over te slaan die debugging bemoeilijken.

De scheiding van verantwoordelijkheden is bewust: breakpoint() en isdebuggerpresent() zijn orthogonale bouwstenen, en breakpointifdebugging() is de gemakkelijke compositie hiervan.

Conclusie

<debugging> is een van die toevoegingen aan C++26 waarbij je je afvraagt waarom het zo lang heeft geduurd. De platformkennis is goed begrepen, elk groot project heeft al zijn eigen versie, en de API bestaat uit slechts drie functies. Soms zijn de meest nuttige toevoegingen aan de standaardbibliotheek juist de eenvoudigste.