0

Is there a way to omit the empty string literals ("") in the argument list of the fmt::format function?

I have the below snippet which gives the desired output:

#include <string>
#include <fmt/core.h>


int main( )
{
    const std::string application_layer_text_head { fmt::format( "{:*<5}[Application Layer]{:*<51}\n\n", "", "" ) };
    fmt::print( "{}", application_layer_text_head );
}

Output:

*****[Application Layer]***************************************************

So instead of writing this: fmt::format( "{:*<5}[Application Layer]{:*<51}\n\n", "", "" ) can we remove the empty literals and write this: fmt::format( "{:*<5}[Application Layer]{:*<51}\n\n" )? I tried it but it failed to compile. Those two literals don't really serve any purpose so I want to find a way to not write them.

Just to clarify, I only want to have 5 * in the beginning and then [Application Layer] and then 51 * and then 2 \n.

digito_evo
  • 3,216
  • 2
  • 14
  • 42
  • @Nicol Bolas I don't only want 5 `*`. Look at the output text. That's what I want. And `fmt::format` works well in this case and is far more efficient than the `operator+` of `std::string`. So it makes sense to use it. – digito_evo Jan 11 '23 at 00:22
  • 2
    It's more efficient than `operator+`, but `operator+=` and `append` avoids most of those costs (reallocating in place, rather than constructing new `string`s and discarding old ones, and `reserve` can be used to ensure it's a single allocation if you really need it). – ShadowRanger Jan 11 '23 at 00:33
  • 1
    I think the question here really is "How do I format this string with 5 `*`s on the left and 51 `*`s on the right?", rather than "how to omit the empty arguments?" Or even more generally: what's the best way of formatting the character `c` `n` times? – Barry Jan 11 '23 at 00:40

2 Answers2

4

Formatting markup is meant for formatting a string with some piece of user-provided data. The particulars of the specialized syntax within formatting can adjust how this formatting works, even inserting characters and the like. But this functionality is meant to be a supplement to the basic act: taking some user-provided object and injecting it into a string.

So no, format doesn't have a mechanism to allow you to avoid providing the user-provided data that is the entire reason format exists in the first place.

It should also be noted that the very meaning of the text after the : in a format specifier is defined based on the type of the object being formatted. The "*<5" means "align to 5 characters using '*' characters to fill in the blanks" only because you provided a string for that particular format parameter. So not only does format not provide a way to do this, it functionally cannot. You have to tell it what type is being used because this is an integral part of processing what "*<5" means.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
1

As noted already, format can't do this. But your worries about string concatenation being expensive are misplaced; repeated application of operator+ is expensive (performs new allocations, copies all existing data and new data, discards old data, over and over), but in-place concatenation with operator+= and append is cheap, especially if you pre-reserve (so you're allocating once up-front and populating, not relying on amortized growth patterns in reallocation to save you). Even without pre-reserve, std::string follows amortized growth patterns, so repeated in-place concatenation is amortized O(1) (per character added), not O(n) in the size of the data so far.

The following should be, essentially by definition, at least as fast as formatting, though at the expense of a larger number of lines of code to perform the pre-reserve to prevent reallocation:

#include <string>
#include <string_view>
#include <fmt/core.h>

using namespace std::literals;

int main( )
{
    // Make empty string, and reserve enough space for final form
    auto application_layer_text_head = std::string();
    application_layer_text_head.reserve(5 + "[Application Layer]"sv.size() + 51 + "\n\n"sv.size());

    // append is in-place, returning reference to original string, so it can be chained
    // Using string_view literals to avoid need for any temporary runtime allocated strings,
    // while still allowing append to use known length concatenation to save scanning for NUL
    application_layer_text_head.append(5, '*').append("[Application Layer]"sv).append(51, '*').append("\n\n"sv);
    fmt::print("{}", application_layer_text_head);
}

If you were okay with some of the concatenations potentially performing reallocation, and a final move construction to move the resources from the temporary reference to a real string, it simplifies to a one-liner:

const auto application_layer_text_head = std::move(std::string(5, '*').append("[Application Layer]"sv).append(51, '*').append("\n\n"sv));

or, given that 5 asterisks is short enough to type, the even shorter/simpler version:

const auto application_layer_text_head = std::move("*****[Application Layer]"s.append(51, '*').append("\n\n"sv));

But keeping it to a two-liner avoids the move construction and is a little safer:

auto application_layer_text_head = "*****[Application Layer]"s;
application_layer_text_head.append(51, '*').append("\n\n"sv);

Yeah, none of those are quite as pretty as a single format literal, even with "ugly" empty placeholders. If you prefer the look of the format string, just pass along the empty placeholders the way you're already doing.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Your one liner constructs a string and then copies it, plus is pretty open to dangling (if you wrote `auto const&` or `auto&&` instead). – Barry Jan 11 '23 at 00:54
  • @Barry: Hmm... You're probably right, since it returns by reference, not value; I suppose `std::move` could be used to at least make it a move construct, and while I can't imagine why you'd declare it a reference directly, it would be a problem if you tried to inline it in a method call that received it by (dangling) reference. So yeah, probably best to two-line it. – ShadowRanger Jan 11 '23 at 02:02
  • Thanks for the suggestion. And speaking of dangling references does it really happen in `const auto application_layer_text_head = std::string(5, '*').append("[Application Layer]"sv).`? Isn't it going to do a move construction cause the object on the right side is temporary (rvalue)? – digito_evo Jan 11 '23 at 08:39
  • @digito_evo The right hand side isn't an rvalue, it's an lvalue. – Barry Jan 11 '23 at 13:32
  • @Barry But why? It's a value without any identifier. How could it be an lvalue? – digito_evo Jan 11 '23 at 13:57
  • @digito_evo Because identifiers aren't the only things that are lvalues. `string::append()` returns an lvalue reference. – Barry Jan 11 '23 at 15:38
  • 2
    @digito_evo: The reason it behaves this way is that it is modifying the str in-place, to avoid copies, and the return value is always an l-value reference to the original str. This allows chaining method calls with no overhead, and doesn't require reassignment when the object already has a name, but it means that, even when you're operating on an r-value initially, it's still returning an l-value reference. They could add [r-value qualified](https://stackoverflow.com/q/19474374/364696) versions of `append`, but they have not done so since the feature was added in C++11. C'est la vie. – ShadowRanger Jan 11 '23 at 17:23