@@ -29,6 +29,7 @@ Cpptrace also has a C API, docs [here](docs/c-api.md).
29
29
- [ Utilities] ( #utilities )
30
30
- [ Configuration] ( #configuration )
31
31
- [ Traces From All Exceptions] ( #traces-from-all-exceptions )
32
+ - [ Removing the ` CPPTRACE_ ` prefix] ( #removing-the-cpptrace_-prefix )
32
33
- [ How it works] ( #how-it-works )
33
34
- [ Performance] ( #performance )
34
35
- [ Traced Exception Objects] ( #traced-exception-objects )
@@ -371,14 +372,14 @@ namespace cpptrace {
371
372
### Traces From All Exceptions
372
373
373
374
Cpptrace provides ` CPPTRACE_TRY ` and ` CPPTRACE_CATCH ` macros that allow a stack trace to be collected from the current
374
- thrown exception object, with no overhead in the non-throwing path:
375
+ thrown exception object, with minimal or no overhead in the non-throwing path:
375
376
376
377
``` cpp
377
378
CPPTRACE_TRY {
378
379
foo ();
379
380
} CPPTRACE_CATCH(const std::exception& e) {
380
- std::cout <<"Exception: "<<e.what()<<std::endl;
381
- std::cout<< cpptrace::from_current_exception().to_string(true)<<std::endl ;
381
+ std::cerr <<"Exception: "<<e.what()<<std::endl;
382
+ cpptrace::from_current_exception ().print() ;
382
383
}
383
384
```
384
385
@@ -396,36 +397,121 @@ API functions:
396
397
- ` cpptrace::from_current_exception ` : Returns a resolved ` const stacktrace& ` from the current exception. Invalidates
397
398
references to traces returned by ` cpptrace::raw_trace_from_current_exception ` .
398
399
400
+ There is a performance tradeoff with this functionality: Either the try-block can be zero overhead in the
401
+ non-throwing path with potential expense in the throwing path, or the try-block can have very minimal overhead
402
+ due to bookkeeping with guarantees about the expense of the throwing path. More details on this tradeoff
403
+ [ below](#performance). Cpptrace provides macros for both sides of this tradeoff:
404
+ - `CPPTRACE_TRY`/`CPPTRACE_CATCH`: Minimal overhead in the non-throwing path (one ` mov ` on x86, and this may be
405
+ optimized out if the compiler is able)
406
+ - ` CPPTRACE_TRYZ ` /` CPPTRACE_CATCHZ ` : Zero overhead in the throwing path, potential extra cost in the throwing path
407
+
408
+ Note: It's important to not mix the ` Z ` variants with the non-` Z ` variants.
409
+
410
+ Unfortunately the try/catch macros are needed to insert some magic to perform a trace during the unwinding search phase.
411
+ In order to have multiple catch alternatives, either ` CPPTRACE_CATCH_ALT ` or a normal ` catch ` must be used:
412
+ ``` cpp
413
+ CPPTRACE_TRY {
414
+ foo ();
415
+ } CPPTRACE_CATCH(const std::exception&) { // <- First catch must be CPPTRACE_CATCH
416
+ // ...
417
+ } CPPTRACE_CATCH_ALT(const std::exception&) { // <- Ok
418
+ // ...
419
+ } catch (const std::exception&) { // <- Also Ok
420
+ // ...
421
+ } CPPTRACE_CATCH(const std::exception&) { // <- Not Ok
422
+ // ...
423
+ }
424
+ ```
425
+
426
+ Note: The current exception is the exception most recently seen by a ` CPPTRACE_CATCH ` macro.
427
+
428
+ ``` cpp
429
+ CPPTRACE_TRY {
430
+ throw std::runtime_error("foo");
431
+ } CPPTRACE_CATCH(const std::exception& e) {
432
+ cpptrace::from_current_exception ().print(); // the trace for std::runtime_error("foo")
433
+ CPPTRACE_TRY {
434
+ throw std::runtime_error("bar");
435
+ } CPPTRACE_CATCH(const std::exception& e) {
436
+ cpptrace::from_current_exception().print(); // the trace for std::runtime_error("bar")
437
+ }
438
+ cpptrace::from_current_exception().print(); // the trace for std::runtime_error("bar"), again
439
+ }
440
+ ```
441
+
442
+ #### Removing the ` CPPTRACE_ ` prefix
443
+
444
+ ` CPPTRACE_TRY ` is a little cumbersome to type. To remove the ` CPPTRACE_ ` prefix you can use the
445
+ ` CPPTRACE_UNPREFIXED_TRY_CATCH ` cmake variable or define ` CPPTRACE_UNPREFIXED_TRY_CATCH ` for the preprocessor:
446
+
447
+ ``` cpp
448
+ TRY {
449
+ foo ();
450
+ } CATCH(const std::exception& e) {
451
+ std::cerr<<"Exception: "<<e.what()<<std::endl;
452
+ cpptrace::from_current_exception ().print();
453
+ }
454
+ ```
455
+
456
+ This is not done by default for macro safety/hygiene reasons. If you do not want ` TRY ` /` CATCH ` macros defined, as they
457
+ are common macro names, you can easily modify the following snippet to provide your own aliases:
458
+
459
+ ``` cpp
460
+ #define TRY CPPTRACE_TRY
461
+ #define CATCH(param) CPPTRACE_CATCH(param)
462
+ #define TRYZ CPPTRACE_TRYZ
463
+ #define CATCHZ(param) CPPTRACE_CATCHZ(param)
464
+ #define CATCH_ALT(param) CPPTRACE_CATCH_ALT(param)
465
+ ```
466
+
399
467
#### How it works
400
468
401
469
C++ does not provide any language support for collecting stack traces when exceptions are thrown, however, exception
402
470
handling under both the Itanium ABI and by SEH (used to implement C++ exceptions on windows) involves unwinding the
403
- stack twice, the first unwind searches for an appropriate ` catch ` handler, the second actually unwinds the stack and
471
+ stack twice. The first unwind searches for an appropriate `catch` handler, the second actually unwinds the stack and
404
472
calls destructors. Since the stack remains intact during the search phase it's possible to collect a stack trace with
405
- zero overhead when the ` catch ` is considered for matching the exception.
473
+ zero overhead when the `catch` is considered for matching the exception. The try/catch macros for cpptrace set up a
474
+ special try/catch setup that can collect a stack trace when considered during a search phase.
406
475
407
476
N.b.: This mechanism is also discussed in [P2490R3][P2490R3].
408
477
409
478
#### Performance
410
479
411
- ` CPPTRACE_CATCH ` internally generates lightweight raw traces when considered in the search phase. These are quite fast
412
- to generate and are only resolved when ` cpptrace::from_current_exception ` is called.
413
-
414
- Currently ` CPPTRACE_CATCH ` always generates a raw trace when considered as a candidate. That means that if there is a
415
- nesting of handlers, either directly in code or as a result of the current call stack, the current stack may be traced
416
- mutliple times until the appropriate handler is found.
417
-
418
- This should not matter for the vast majority applications given that performance very rarely is critical in throwing
419
- paths, how exception handling is usually used, and the shallowness of most call stacks. However, it's something to be
420
- aware of.
480
+ The fundamental mechanism for this functionality is generating a trace when a catch block is considered during an
481
+ exception handler search phase. Internally a lightweight raw trace is generated upon consideration, which is quite
482
+ fast. This raw trace is only resolved when `cpptrace::from_current_exception` is called, or when the user manually
483
+ resolves a trace from `cpptrace::raw_trace_from_current_exception`.
484
+
485
+ It's tricky, however, from the library's standpoint to check if the catch will end up matching. The library could simply
486
+ generate a trace every time a `CPPTRACE_CATCH` is considered, however, in a deep nesting of catch's, e.g. as a result of
487
+ recusion, where a matching handler is not found quickly this could cause notable overhead due to tracing the stack
488
+ multiple times. Thus, there is a performance tradeoff between a little book keeping to prevent duplicate tracing or
489
+ biting the bullet, so to speak, in the throwing path and unwinding multiple times.
490
+
491
+ > [!TIP]
492
+ > The choice between the `Z` and non-`Z` (zero-overhead and non-zero-overhead) variants of the exception handlers should
493
+ > not matter 99% of the time, however, both are provided in the rare case that it does.
494
+ >
495
+ > `CPPTRACE_TRY`/`CPPTRACE_CATCH` could only hurt performance if used in a hot loop where the compiler can't optimize
496
+ > away the internal bookkeeping, otherwise the bookkeeping should be completely negligible.
497
+ >
498
+ > `CPPTRACE_TRYZ`/`CPPTRACE_CATCHZ` could only hurt performance when there is an exceptionally deep nesting of exception
499
+ > handlers in a call stack before a matching handler.
500
+
501
+ More information on performance considerations with the zero-overhead variant:
502
+
503
+ Tracing the stack multiple times in throwing paths should not matter for the vast majority applications given that:
504
+ 1. Performance very rarely is critical in throwing paths and exceptions should be exceptionally rare
505
+ 2. Exception handling is not usually used in such a way that you could have a deep nesting of handlers before finding a
506
+ matching handler
507
+ 3. An that most call stacks are fairly shallow
421
508
422
509
To put the scale of this performance consideration into perspective: In my benchmarking I have found generation of raw
423
510
traces to take on the order of `100ns` per frame. Thus, even if there were 100 non-matching handlers before a matching
424
511
handler in a 100-deep call stack the total time would stil be on the order of one millisecond.
425
512
426
- It's possible to avoid this by adding some bookkeeping to the ` CPPTRACE_TRY ` block. With the tradeoff between
427
- zero-overhead try-catch in the happy path and a little extra overhead in the unhappy throwing path I decided to keep
428
- try-catch zero-overhead. Should this be a concern to anyone, I'm happy to facilitate both solutions.
513
+ Nonetheless, I chose a default bookkeeping behavior for `CPPTRACE_TRY`/`CPPTRACE_CATCH` since it is safer with better
514
+ performance guarantees for the most general possible set of users.
429
515
430
516
### Traced Exception Objects
431
517
0 commit comments