clang-tidy-modernize-use-std-print
Clang 17 has recently been released.
It includes the new modernize-use-std-print check, which is the
culmination of work I started two years ago. In short, it turns lines
like:
fprintf(stderr, "The %s is %3d\n", description.c_str(), value);
into:
std::println(stderr, "The {} is {:3}", description, value);
The full documentation for the check is available on the clang-tidy
web site, but here are some examples. Given a file named
example.cpp
containing:
#include <cstdio>
#include <string>
void print_row(int quantity, const std::string &name, double mass, const std::string &description)
{
printf("| %3d | %10s | %6.3fkg | %-30s |\n", quantity, name.c_str(), mass, description.c_str());
}
int main()
{
print_row(4, "carrots", 0.25, "Long orange things");
print_row(9, "sprouts", 0.1, "Roundish green things");
print_row(60, "peas", 0.15, "Round green things");
print_row(10, "beans", 0.05, "Long green things");
}
then running
clang-tidy '-checks=-*,modernize-use-std-print' -fix example.cpp -- -std=c++23
will result in the file being rewritten to:
#include <cstdio>
#include <print>
#include <string>
void print_row(int quantity, const std::string &name, double mass, const std::string &description)
{
std::println("| {:3} | {:>10} | {:6.3f}kg | {:30} |", quantity, name, mass, description);
}
int main()
{
print_row(4, "carrots", 0.25, "Long orange things");
print_row(9, "sprouts", 0.1, "Roundish green things");
print_row(60, "peas", 0.15, "Round green things");
print_row(10, "beans", 0.05, "Long green things");
}
The check has done several things:
- The
<print>
header is now included. (The
inclusion of <cstdio>
has not been removed since it
may be required for other reasons.)
- The
printf
call has been replaced with
std::println
.
- The
\n
has been removed from the end of the format
string since std::println
will add a newline
automatically.
- The format string has been rewritten to use the new C++ format
language.
- The now-unnecessary calls to
std::string::c_str()
have
been removed.
Despite all this, the output is the same.
Of course, most of us aren’t lucky enough to be using C++23 yet.
Helpfully, the author of the original std::print
and
std::format
feature proposal has the excellent {fmt} library which can be
used with earlier C++ versions (though sometimes with less
functionality.) The check can be customised to use calls to the {fmt}
equivalents instead. To make this easier, this time we’re going to
create a .clang-tidy
configuration file in the same
directory as example.cpp
since passing options on the
command line is rather unwieldy:
Checks: -*, modernize-use-std-print
CheckOptions:
modernize-use-std-print.PrintHeader: '<fmt/core.h>'
modernize-use-std-print.ReplacementPrintFunction: 'fmt::print'
modernize-use-std-print.ReplacementPrintlnFunction: 'fmt::println'
Now, we no longer need to enable C++23 when running clang-tidy, and
it will automatically pick up the configuration:
clang-tidy -fix example.cpp --
results in:
#include <fmt/core.h>
#include <cstdio>
#include <string>
void print_row(int quantity, const std::string &name, double mass, const std::string &description)
{
fmt::println("| {:3} | {:>10} | {:6.3f}kg | {:30} |", quantity, name, mass, description);
}
int main()
{
print_row(4, "carrots", 0.25, "Long orange things");
print_row(9, "sprouts", 0.1, "Roundish green things");
print_row(60, "peas", 0.15, "Round green things");
print_row(10, "beans", 0.05, "Long green things");
}
This is the same as before, except for the header inclusion and the
use of fmt::println
rather than
std::println
.
The check will only convert calls if it thinks that it can do so
without changing the behaviour. However, in order to enforce this it’s
necessary to enable the StrictMode
feature. This ought to
only affect corner cases, where you may actually want the change in
behaviour anyway. Consider this code:
int count = -42;
printf("%u\n", count);
On the majority of modern machines this will print
4294967254
but either count
being
int
or using %u
may have been unintentional as
GCC’s -Wformat
doesn’t complain about this. Running:
clang-tidy '-checks=-*,modernize-use-std-print' -fix example.cpp -- -std=c++23
will yield:
int count = -42;
std::println("{}", count);
which will always print -42
.
If you really want the same output as before then it is necessary to
enable StrictMode
with something like:
clang-tidy -config="{CheckOptions: [{key: StrictMode, value: true}]}" '-checks=-*,modernize-use-std-print' -fix example.cpp -- -std=c++23
(or the equivalent in the .clang-tidy
file) which
yields:
int i = -42;
std::println("{}", static_cast<unsigned int>(i));
When appropriately configured, the check is also capable of
converting from fmt::printf
and absl::PrintF
.
When doing so, no casts are added by StrictMode
since those
functions print based on the real type rather than the format
string.
The implementation of the check in Clang v17.0.1 unfortunately
contains a bug when
supplying field widths and precision in arguments. The check will
generate corrupt output (that probably won’t compile) when arguments
need to be reordered in combination with the addition of cases (for
StrictMode
) or c_str()
removal. For
example:
std::string s{"world"};
printf("%*s\n", 20, s.c_str());
ought to be converted to:
std::string s{"world"};
std::println("{:>{}}", s, 20);
This bug has been fixed on the main branch
and that fix ought to be present in Clang v18.0.0 when it is
released.