Wednesday, 20 September 2023

New modernize-use-std-print clang-tidy check in Clang 17

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:

  1. The <print> header is now included. (The inclusion of <cstdio> has not been removed since it may be required for other reasons.)
  2. The printf call has been replaced with std::println.
  3. The \n has been removed from the end of the format string since std::println will add a newline automatically.
  4. The format string has been rewritten to use the new C++ format language.
  5. 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.