Skip to content

Added macros that returns formatted value - #96

Open
JoyHak wants to merge 3 commits into
renatoGarcia:masterfrom
JoyHak:format-return-macros
Open

Added macros that returns formatted value#96
JoyHak wants to merge 3 commits into
renatoGarcia:masterfrom
JoyHak:format-return-macros

Conversation

@JoyHak

@JoyHak JoyHak commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

I wanted to combine the formatting capabilities of IceCream with{fmt} and be able to quickly output and format objects. Since IceCream does not support "{} {}" formatting, such formatting can be created with IC_R (return) and IC_FR (format and return):

auto v = std::vector<int>{10, 11, 12, 13, 14};
auto str = IC_R(v);  // ic| v: [10, 11, 12, 13, 14]
auto str = IC_FR("[0:2]", v);  // ic| v: [10, 11, 12]
IC_CONFIG.prefix("");
auto v = std::vector<int>{10, 11, 12, 13, 14};
auto i = 21;
fmt::print("Pushing {} to the {}", IC_FR("#x", i), IC_R(v))
// Pushing i: 0x15 to the v: [10, 11, 12, 13, 14]
IC_CONFIG.prefix("");
S s = {3.14, {1,2,3}};    
fmt::print("Testing class {}", IC_R(s));  
// Testing class s: {f: 3.14, ii: [1, 2, 3]}

I'm trying to reuse IceCream formatting without rewriting anything. The macros passed local tests successfully, and I used them in my projects.

@renatoGarcia

Copy link
Copy Markdown
Owner

Hi @JoyHak , thanks for your code.

What exactly is the pain point you're trying to address here?
I'm wary about adding two more macros to icecream for a feature that I believe is outside the library's scope.

The intent of icecream-cpp is to deliver a quick and practical way to inspect variable contents and code paths. After inspecting/debugging the code, icecream-cpp usage should be removed from the project before delivery. The output generated by icecream-cpp is meant to be consumed by the programmer during development, not by the end user.

Those objectives differ from the {fmt} library, which generates messages to be printed to users, saved in log files, etc.

In your examples, when you write:

fmt::print("Pushing {} to the {}", IC_FR("#x", i), IC_R(v))

why is this better than just:

fmt::print("Pushing {:#x} to the {}", i, v)

I'm open to discussion and I'm open to being convinced otherwise. But for now, I think that if you're already using fmt::print with a custom message and you really need the variable name, you're better off with:

fmt::print("Pushing i: {:#x} to the v: {}", i, v)

The only place where I can see that integrating icecream-cpp with {fmt} adds value is formatting ranges with Python-inspired slicing and printing structs with "Clang dump struct". But even there, the focus is on the printing itself, not on the variable name together with that printing. I imagine that a function like:

template <typename T>
auto ic_print(std::string_view fmt, T&& arg) -> std::string

that would return just the printed value (no prefix, no variable name) would solve the problem better.

What do you think?

@JoyHak

JoyHak commented Mar 15, 2026

Copy link
Copy Markdown
Contributor Author

What exactly is the pain point you're trying to address here?

Those objectives differ from the {fmt} library, which generates messages to be printed to users, saved in log files, etc.

I'm trying to extend the IceCream library, because it provides some uniq formatting that fmt doesn't (see below).

The output generated by icecream-cpp is meant to be consumed by the programmer during development, not by the end user.

For debug builds it can be useful to mix IceCream with some Print() macro that I'm using.

why is this better than just:
fmt::print("Pushing {:#x} to the {}", i, v)

I can't slice any range using fmt, I need to implicitly use ranges and take some values using pipe syntax. Also this syntax requires some additional text like i: ... v: ..., or even v.push_back(): ... and IC handles this text automatically which increases writing speed:

the Icecream-cpp library perhaps is one of the few libraries that you should spend more time writing code with it than reading code with it.

Furthermore IceCream provides structure and class dump (I'm using clang 21 right now), and that's really powerful.

But all this helpful information can't be mixed with support text like testing my class. Any literals are displayed separately and additional text is just ignored: IC("additional text [0:2]", v)...

what do you think?

In addition, I think that IceCream requires at least some formatting that can simplify reading stdout:
ic| updating vector v: ..., testing my class c: .... I achieved this by mixing text {} format with IC_R() that handles variable/object content.

@renatoGarcia

Copy link
Copy Markdown
Owner

OK @JoyHak, I agree with you here. It would be nice to customize a message before showing the value. It would help the debugging adding context beyond the variable name.

However, from a design perspective, I'm not comfortable with adding a new pair of macros returning a string instead of printing it. The problem is that if we have these two macros at the same logical level in the API as the other six (IC, IC_A, IC_V, IC_F, IC_FA, IC_FV), it will move the Icecream-cpp library uncomfortably close to a formatting library. From the eyes of an onlooker, that could put Icecream-cpp and {fmt} as having a bigger overlap in their problem domain. Something that I wish to avoid.

If someone believes that Icecream-cpp works like a formatting library, and uses it to that purpose, it would lead them to a bad experience. We have design goals possibly at odds with the design goals of a good formatting library.

Moreover, we use {fmt} (or STL formatting) internally. If we use {fmt} to generate a string, that the user will send to {fmt} again to print. I don't know, my spider-sense is telling me that this isn't a good design.

In addition, I think that IceCream requires at least some formatting that can simplify reading stdout:
ic| updating vector v: ..., testing my class c: ...

Sure, all that text to justify this: I'm inclined to go in the line you have suggested here. We can add the feature to print the additional text to Icecream itself.

Some details can change while I think better about it, but a rough idea is this: we change the "output formatting" syntax to support it.

  1. The previous syntax is still valid.
auto a = int{42};
auto b = int{20};
IC_F("#X", a, IC_("d", b));

ic| a: 0X2A, b: 20
  1. If we have replacement fields ({}) in the formatting string, then all the extra text will be the additional text. I'm thinking in something like:
IC_F("Testing my value $name -> {#X}", a);

ic| Testing my value a -> 0X2A
  1. When reading the text, $name will be always replaced by the variable name. {} will be always replaced by the variable value. The formatting string goes as usual {#x} (we still don't have the ":" like {:#x})

  2. If present multiple times, they will be print multiple times.

IC_F("Testing {} my $name value $name -> {#X}", a);

ic| Testing 42 my a value a -> 0X2A
  1. We look for any presence of a {} in the text ("Testing {}"), if we find it, the text is a full will additional text. If we don't find any {} the whole text is the formatting text. ("#X" is equivalent to "$name: {#X}")

  2. "$$name" prints literal "$name" and "{{}}" prints literal "{}". To escape the substitutions.

This is my rough solution idea. What do you think about it? Can you see any problems with it? Do you have any improvements?

@JoyHak

JoyHak commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

{ } serves as format placeholder, so we can re-use it for meta information about variable itself:

  • name (vec, a)
  • qualified name (namespace::vec, b::a)
  • type (vector)
  • qualified type (std::vector)
  • ... (anything that selected CXX standart or compiler supports).
    We can specify meta outside of the format string, pair each meta specifier with format specifier using idx: {id:meta}.
    Example:
auto vec = std::vector<int>{10, 11, 12};
IC_F("{1:name} contains {1:[1:2]}", v);  // vec contains [10, 11]
IC_F("{1:name} is the {1:type}", v);       // vec is the vector<int>

int a = 1, int b = 2
IC_F("{1:name} at the {1:address}; {2:name} at the {2:address}", a, b);       // a at the 000000FFFA..., b at the 000000CFFA...

@JoyHak

JoyHak commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

If multiple meta specifiers are present, they should be applied sequentially to each variable (one meta specifier per variable), similar to the format specifier that does not include a variable index:

auto a = 1, auto b = 2
IC_F("{:d} != {:d}", a, b);  // 1 != 2 - no index, apply format one by one
IC_F("{:name} and {:name}", a, b);  // a and b - no index too, apply meta one by one
IC_F("{:name} is {:d} and {name} is {:d}", a, b);  // a is 1 and b is 2

@renatoGarcia

Copy link
Copy Markdown
Owner

IC_F("{1:name} at the {1:address}; {2:name} at the {2:address}", a, b);

This is something I want to explicitly avoid, having more than one variable mapped to the same formatting string. Doing so would break Icecream-cpp internal logic, such as the ability to break long lines with proper indentation. The current design is: one variable produces one string, then we merge all strings together for output. Moreover, it would move us uncomfortably close to {fmt}, adding complexity we'd need to maintain.

This is a subjective boundary: where do we draw the line past which we've gone too far into the {fmt} domain? I believe we should limit one formatting string to one variable. In a one-to-one mapping:

IC(IC_("{:name} at the {:address}", a), IC_("{:name} at the {:address}", b));

And since the formatting strings are identical, this simplifies to:

IC_F("{:name} at the {:address}", a, b);

That said, I like the idea of relying on what the C++ standard provides for meta information. C++26's reflection support may offer some useful features, I will research about it. We just need take care to avoid potential ambiguities. For example, if we have:

IC_F("{:d} != {:d}", a, b);
IC_F("{:name} and {:name}", a, b);

what if name is also a valid format specifier for a hypothetical type of a and b? In that case, {:name} would be ambiguous: does it substitute the variable name, or is it the name format specifier for decltype(a)?

So we need a syntax where the "meta information id" and the "format specifier" are unambiguous. Since we map one variable to one output, we don't need numeric indices or any other variable selector.

In this scheme, both the variable's "name" and its "value" are meta information ids. This suggests:

IC_F("{name} -> {value:#x}", my_int);

This leads to the reasoning in my last message, where I suggested:

IC_F("$name -> {#x}", my_int);

The name meta id doesn't support any format specifier. In fact, no meta id other than value does. Since all meta ids except value are simple variable substitutions, $name (or $type with other meta ids, etc.) better represents that operation. The {} syntax should be reserved for value, since it's the only one that supports format specifiers.

Alternatively, we could adopt a syntax like:

{<meta>:<fmt>} and if {:<fmt>} <meta> defaults to "value"

So:

IC_F("{type} - {name} -> {value:#x}", my_int);
IC_F("{type} - {name} -> {:#x}", my_int);

I'll think more about these two syntax options. I'm currently split between them.

Regardless, thank you very much for your discussion here! I believe it will inspire some very nice features.

@JoyHak

JoyHak commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

I thought the new characters like $name might break backwards compatibility (users may use $ for some output), so my previous idea was about adding meta specifier at the beginning/end of the main format string: {meta~index:format} or {index:format~meta}, where meta is something specific to the variable itself

Or it can be something like {{meta}:index:format}, e.g. {name}:1:d or {type}:1:[1:7]. Spaces/other delimiters can be added automatically depending on meta:
IC_F("{type}:1:[1:3]", v -> std::vector<int> [1,2,3].

It's simpler then pairs of formatting strings, but it doesn't allows to specify multiple meta specifiers, e.g. both type and name. So I decided to split it into "pair" of formatting strings, linked by index. I don't like this version very much because it's still complex even without meta id: {meta:format}

I believe we should limit one formatting string to one variable.

You mean the whole formatting string like "{type} - {name} -> {value:#x}" or placeholders only {value:#x}? I need to understand it before commenting this version:

IC_F("{type} - {name} -> {value:#x}", my_int);
IC_F("{type} - {name} -> {:#x}", my_int);

@JoyHak

JoyHak commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

The {} syntax should be reserved for value, since it's the only one that supports format specifiers.

I believe that it may support meta specifiers without formatting, i.e. you can't specify {name:d}, only {name} as separate placeholder.

The idea is to keep {} as a placeholder for everything, but limit it's capabilities for meta information: only literals like "name" inside {}, no formatting. If no meta information is requested, it simplifies to {:fmt} like here:

{<meta>:<fmt>} and if {:<fmt>} <meta> defaults to "value"

IC_F("{type} - {name} -> {:#x}", my_int);

@renatoGarcia

renatoGarcia commented Apr 23, 2026

Copy link
Copy Markdown
Owner

My last message was confusing. Let me try to explain my idea in more precise terms.

I thought the new characters like $name might break backwards compatibility (users may use $ for some output),

In theory, this wouldn't be a big problem. We shouldn't break backward compatibility just for fun, but following the design principle:

"It is designed for use during development and debugging, not for shipping with the released product"

If we are torn between backward compatibility and a better API, in general having a better API should win. However, I'm with you here. Having just the {} marker is the consistent choice. This gives us one consistent symbol for replacement fields, rather than two variants.

To fix the terminology, in this code line:

IC_F("I'm printing {value:#x}", my_int);

The whole "I'm printing {value:#x}" string will be called format string and the "{value:#x}" substring will be called replacement field.

My point is that we should restrict each format string to a single variable. That will avoid adding complexity to the library when breaking long lines, parsing the string, etc.

With this one-to-one mapping, we no longer need to specify (with indexes or whatever) which variable is being referenced by each replacement field. There is only one variable being printed to that format string.

The syntax would be:

format_string     ::=  full_string | short_string
full_string       ::=  (text | replacement_field)+
short_string      ::=  [":" format_spec]
text              ::=  <A non empty string not containing "{...}" or ":">
replacement_field ::=  "{" [meta] [":" format_spec] "}"
meta              ::=  "name" | "value" | "type" | ...
format_spec       ::=  <formatting specification contingent on the meta>

Where meta is what information will be printed: the variable name, the variable value, the variable type, etc. The possible values can be extended when we see a new useful feature.

A short string ":d" will be interpreted as "{name}: {value:d}", to keep a similar semantic to current implementation. An empty short string "" will be interpreted as "{name}: {value}".

meta is optional. If missing, value will be assumed. So {} is the same as {value}, and {:d} is the same as {value:d}.

A "{{}}" text won't be interpreted as a replacement field, and will print "{}". And a "::" string will print ":".

For now, no other meta besides value has a format_spec, but perhaps we can add specs to print the type name with or without namespaces for example. We can always add this later if it proves useful.

The same meta can appear multiple times, and will be replaced accordingly. So this will be possible:

IC_F("{name} in decimal {:d} and hex {:#x}", my_int);

This is my current thinking. I believe we are converging to something cool.

@JoyHak

JoyHak commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

This is my current thinking. I believe we are converging to something cool.

Awesome, I like this design!

With this one-to-one mapping, we no longer need to specify (with indexes or whatever) which variable is being referenced by each replacement field. There is only one variable being printed to that format string.

The syntax would be...

This big specification is a bit confusing for a newbie, so I want to suggest how to change it a little bit. Instead of strictly describing the specification for the new formatting, the main elements will be described using ::= and everything else will be described using plain text with examples.

Possible documentation

Macros like IC_F() accept literal formatting string like "my var {name:...}" that may contain literal text ("my var") and replacement fields ("{name:...}") that will be replaced with the necessary information (e.g. {name} will be replaced with var. name) depending on the text and formatting inside the curly brackets {}.

This is why it's called formatting: the given string is formatted (changed) according to some rules you specify in the replacement fields {} dynamically during program execution.

To better understand this, let's take a look at example output depending on the given string:

auto v = 1.1;
IC_F("{:#x}", v);  // 0x1 - format as hexadecimal
IC_F("{:d}", v);  // 1 - format as decimal
IC_F("{name} is {}", v);  // v is 1.1

As you can see, the output value changes (formatted) according to the given rules, for which we allow the following syntax:

replacement_field ::=  "{" [meta] [":" format_spec] "}"
meta              ::=  "name" | "value" | "type" | ...
format_spec       ::=  <formatting specification>

Here's your explanation about meta:

Where meta is what information will be printed: the variable name, the variable value, the variable type, etc., and format_specification is the rule how to format variable value (meta info cannot be formatted). Read more about available formatting strategies here. This jump to another section is bad, I think we need to change the logic/text order in this paragraph.

For example, you can get information about what variable you are using in a given piece of code. This information can be useful for debugging variables with the same name in different scopes { ... }. Not sure if we need to include this info here.

IC_F("{name} is {value}", v); // v is 1.1
IC_F("{name} is {type}", v); // v is int

meta (like "name" here) is optional. If missing, value will be assumed. So {} is the same as {value}, and {:d} is the same as {value:d}:

IC_F("{name} is {value}", v); // v is 1.1
IC_F("{name} is {}", v); // v is 1.1 - same as above

A "{{}}" text won't be interpreted as a replacement field, and will print "{}". And a "::" string will print ":".

IC_F("{{type}}:: {type}", v); // {type}: int

It's not clear to me: :: should be specified in the {} or outside of {} to print : ?

You can completely avoid meta or formatting if you don't need it:

IC_F("I don't have vars", v); // I don't have vars
// plain text, no formatting

...I think I need to commit the changes to readme to see how new docs looks with the current docs. So far, I'm not sure which section(-s) needs to be updated to not scare the new user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants