Never use cout/printf to debug again
[![CI.badge]][CI.link] [![LICENSE.badge]][LICENSE.link]
Icecream-cpp is a little (single header) library to help with print debugging in C++11 and later.
Tracking execution flow:
auto roll_attack(int roll) -> void
{
std::cout void
{
IC();
if (roll == 20)
IC();
else
IC();
}and will print something like:
ic| combat.cpp:12 in "void roll_attack(int)"
ic| combat.cpp:14 in "void roll_attack(int)"Inspecting variables:
std::cout {U'α', U'β', U'γ', U'δ', U'ε', U'ζ'};
IC_F("[1:-2]", v);will print:
ic| v: [1:-2]->['β', 'γ', 'δ']Debugging range pipelines. Insert IC_V() to inspect data
flowing through STL ranges or
Range-v3 pipelines:
auto rv = std::vector{1, 0, 2, 3, 0, 4, 5}
| vws::split(0)
| IC_V()
| vws::enumerate;When iterating on rv, we will see:
ic| range_view_63:16[0]: [1]
ic| range_view_63:16[1]: [2, 3]
ic| range_view_63:16[2]: [4, 5]This library was inspired by the original Python IceCream library.
Contents
Icecream-cpp is a throwaway library. It is designed for use during development and debugging, not for shipping with the released product. It complements debuggers rather than replacing them, sometimes print debugging is simply faster or more practical.
Because of this, Icecream-cpp is one of the few libraries where you will spend more time writing code with it than reading code with it. Our design goals are expressivity and conciseness: print any variable, format it as desired, with as little code as possible.
Icecream-cpp is a single-file, header-only library with only the STL as dependency. The
simplest way to use it is to copy icecream.hpp into your project.
To install system-wide with CMake support, run from the project root:
mkdir build
cd build
cmake ..
cmake --install .Released versions are available on Conan. See example_project/conanfile.txt for a usage example.
Icecream-cpp can be included as a flakes input:
inputs.icecream-cpp.url = "github:renatoGarcia/icecream-cpp";The package can be referenced directly:
inputs.icecream-cpp.packages.${system}.defaultAlternatively, the flake provides an overlay for use when importing nixpkgs:
import nixpkgs {
system = "x86_64-linux";
overlays = [
icecream-cpp.overlays.default
];
}This adds an icecream-cpp derivation to the nixpkgs attribute set.
See example_project/flake.nix for a working example.
With CMake:
find_package(icecream-cpp)
target_link_libraries( PRIVATE icecream-cpp::icecream-cpp)Where `` is the executable or library being compiled.
Then include the header:
#include
int main(int, char*[])
{
auto x = 42;
auto name = std::string{"foo"};
IC(x, name); // prints: ic| x: 42, name: "foo"
return 0;
}The main Icecream-cpp functions are IC,
IC_A, and IC_V;
together with their respective counterparts IC_F, IC_FA, and IC_FV; that behave the
same but accept an output formatting string as their first argument.
IC is the simplest of the Icecream-cpp functions. If called with no arguments it will
print the prefix, the source file name, the current line number, and the
current function signature. The code:
auto my_function(int foo, double bar) -> void
{
// ...
IC();
// ...
}will print:
ic| test.cpp:34 in "void my_function(int, double)"If called with arguments it will print the prefix, those argument names, and their values. The code:
auto v0 = std::vector{1, 2, 3};
auto s0 = std::string{"bla"};
IC(v0, s0, 3.14);will print:
ic| v0: [1, 2, 3], s0: "bla", 3.14: 3.14The variant IC_F behaves the same as the IC function, but accepts an output
formatting string as its first argument.
To print the data flowing through a range views pipeline (both with STL
ranges and
Range-v3), we can use either the IC_V or
IC_FV functions, which will lazily print any input coming from the previous view. IC_FV behaves the same as IC_V, but accepts a format string
(same syntax as the range format string) as its first argument. Since these functions will be placed within a range views pipeline, the
printing will be done lazily, while each element is generated. For instance:
namespace vws = std::views;
auto v0 = vws::iota('a') | vws::enumerate | IC_V() | vws::take(3);
for (auto e : v0)
{
//...
}In this code nothing will be immediately printed when v0 is created, just when iterating
over it. At each for loop iteration one line will be printed, until we have the output:
ic| range_view_61:53[0]: (0, 'a')
ic| range_view_61:53[1]: (1, 'b')
ic| range_view_61:53[2]: (2, 'c')[!NOTE] The Icecream-cpp will enable its support to Range-v3 types either if the "icecream.hpp" header is included some lines after any Range-v3 header, or if the
ICECREAM_RANGE_V3macro was declared before the "icecream.hpp" header inclusion. This is discussed in details at the third-party libraries section.
IC_V has the signature IC_V(name, projection), and IC_FV has
IC_FV(fmt, name, projection). In both, the name and projection parameters are
optional.
The same syntax as described at range types format string.
The variable name used by the view when printing. The printing layout is: []: . If the name parameter is not used, the default value to `` is
range_view_.
The code:
vws::iota('a') | vws::enumerate | IC_V("foo") | vws::take(2);when iterated over will print:
ic| foo[0]: (0, 'a')
ic| foo[1]: (1, 'b')A callable that will receive as input a const reference to the current element at the range views pipeline, and must return the actual value to be printed.
The code:
vws::iota('a') | vws::enumerate | IC_V([](auto e){return std::get(e);}) | vws::take(2);when iterated over will print:
ic| range_view_61:53[0]: 'a'
ic| range_view_61:53[1]: 'b'[!NOTE] The
IC_Vfunction will forward to the next view the exact same input element, just as it was received from the previous view. No action done by theprojectionfunction will have any effect on that.
IC (and IC_F) returns no value when called
with zero or multiple arguments. When called with exactly one argument, it returns that
argument.
This allows using IC to inspect a function argument at the call site without changing
the code structure. In the code:
my_function(IC(MyClass{}));the MyClass object will be forwarded to my_function as if the IC function wasn't
there. The my_function will continue receiving a rvalue reference to a MyClass object.
This approach, however, is not practical when the function has multiple arguments. In the code:
another_function(IC(a), IC(b), IC(c), IC(d));besides writing four times the IC function, the printed output will be split in four
lines. Something like:
ic| a: 1
ic| b: 2
ic| c: 3
ic| d: 4To work around that, there is the IC_A function. IC_A behaves exactly like the IC
function, but receives a callable
as its first argument, and will call it using the remaining arguments, printing all of
them before that. That previous example code could be rewritten as:
IC_A(another_function, a, b, c, d);and this time it will print:
ic| a: 1, b: 2, c: 3, d: 4The IC_A function will return the same value returned by the callable. The code:
auto mc = std::make_unique();
auto r = IC_A(mc->my_function, a, b);behaves exactly the same as:
auto mc = std::make_unique();
auto r = mc->my_function(a, b);but will print the values of a and b.
The IC_FA variant does the same as the IC_A function, but accepts an output
formatting string as its first argument, just before the callable
argument.
It is possible to configure how a value must be formatted while printing. The following code:
auto a = int{42};
auto b = int{20};
IC_F("#X", a, b);will print:
ic| a: 0X2A, b: 0X14if using the IC_F variant instead of the plain IC function. A
similar result would be achieved if using IC_FA and IC_FV in place of
IC_A and IC_V
respectively.
When using the formatting function variants (IC_F and IC_FA), the same formatting
string will be applied by default to all of its arguments. That could be a problem if we
need to have distinct formatting to each argument, or if the arguments have multiple types
with mutually incompatible format syntaxes. Therefore, to set a distinct formatting string to a
specific argument we can wrap it with the IC_ function. The code:
auto a = int{42};
auto b = int{20};
IC_F("#X", a, IC_("d", b));will print:
ic| a: 0X2A, b: 20The IC_ function can also be used within the plain IC (or IC_A) function:
auto a = int{42};
auto b = int{20};
IC(IC_("#x", a), b);will print:
ic| a: 0x2a, b: 20The last argument in an IC_ function call is the one that will be printed, all other
arguments coming before the last will be converted to a string using the
to_string function
and concatenated to the resulting formatting string.
auto a = float{1.234};
auto width = int{7};
IC(IC_("* void
{
IC_CONFIG.line_wrap_width(20);
IC_CONFIG_SCOPE();
IC_CONFIG.context_delimiter("|");
IC_CONFIG.show_c_string(true);
{
IC_CONFIG_SCOPE();
IC_CONFIG.show_c_string(false);
// A
}
// B
}At line A, the value of IC_CONFIG's line_wrap_width, context_delimiter, and
show_c_string will be respectively: 20, "|", and false.
After the closing of the innermost scope block, at line B, the value of IC_CONFIG's
line_wrap_width, context_delimiter, and show_c_string will be respectively: 20,
"|", and true.
The reading and writing operations on IC_CONFIG objects are thread safe.
[!NOTE] Any modification in an
IC_CONFIG, other than to the global instance, will be seen only within the current scope. As a consequence, those modi
No open issues yet, or sync has not completed.