Как вывести форматированный текст на экран в C++

33449d12d20f7747356e476fe03201a3.png

Рано или поздно у каждого программиста появляется желание вывести форматированный текст на экран. Немудрено, у пляшущих на экране буковок есть свой неповторимый шарм, а факт форматированности добавляет им еще и загадочности — мы можем даже не догадываться, что в точности будет напечатано.

Но как сделать это оптимально и кроссплатформенно?

Стойте, стойте, но у нас ведь все для этого есть

Казалось бы, если мы желаем вывести форматированный текст на экран, в C++20 у нас есть много способов это сделать: с помощью расово верных потоков (std::cout и компания), классического C API для форматированного вывода (std::printf и его братья), а также используя std::format в сочетании с функциями наподобие std::fputs. Но все они обладают своими недостатками.

Рассмотрим следующий код:

std::cout << "The answer is " << 42 << ".\n";

Даже с выключенной синхронизацией он не является оптимальным в том плане, что вырождается в множество вызовов функций и выполняет преобразования, связанные с локалями, даже тогда, когда они нам совсем не нужны:

  push rbx
  mov rbx, qword ptr [rip + std::cout@GOTPCREL]
  lea rsi, [rip + .L.str.1]
  mov edx, 14
  mov rdi, rbx
  call std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long)@PLT
  mov rdi, rbx
  mov esi, 42
  call std::basic_ostream >::operator<<(int)@PLT
  lea rsi, [rip + .L.str.2]
  mov edx, 2
  mov rdi, rax
  pop rbx
  jmp std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long)@PLT

Казалось бы, лаконичный и эффективный код может быть получен с использованием std::printf:

std::printf("The answer is %d.\n", 42);
  lea rdi, [rip + .L.str]
  mov esi, 42
  xor eax, eax
  jmp printf@PLT

Однако он все так же безальтернативно зависим от текущей локали и, кроме того, плохо дружит с современной, окружающей нас, реальностью. Рассмотрим следующий пример:

void greet(std::string_view name) {
  std::printf("Hello, %s!", name.data());
}

В общем случае этот код приводит к неопределенному поведению, так как %s спецификатор требует нуль-терминированную строку, тогда как строка, переданная через std::string_view, нуль-терминированной быть не обязана.

И, наконец, рассмотрим последний случай:

auto msg = std::format("The answer is {}.\n", 42);
std::fputs(msg.c_str(), stdout);

Будучи лишенным недостатков как потоков, так и std::printf, он все же конструирует не нужную нам временную строку, требует вызовов c_str и отдельной I/O функции. Кажется, будто можно сделать лучше.

Кроме того, говоря исключительно о проблемах производительности и совместимости, мы совершенно упустили из виду очень важную проблему, форматирование Unicode строк:

std::cout << "Привет, κόσμος!";

Если на большинстве Linux и MacOS систем вышеприведенный код выведет ровно то, что мы от него и ожидаем, то на Windows мы обречены увидеть кракозябры, например: ╨ƒ╤Ç╨╕╨▓╨╡╤é, ╬║╧î╧â╬╝╬┐╧é!, как бы мы этого не пытались избежать различными флагами компиляции.

Так что же, неужели в современных плюсах нет способа вывести форматированный текст без лишних накладных расходов так, чтобы он хотя бы на самых популярных современных системах (Linux, MacOS, Windows) отображался корректно? Даже Python так может, а мы не можем?

Спешу вас обрадовать, с C++23 мы можем не хуже, чем Python

Ведь у нас появился std::print, полностью дружащий с современными плюсами, абсолютно типобезопасный, выводящий UTF-8 корректно на всех системах, его поддерживающих, и при этом не менее эффективный, чем std::printf:

std::print("The answer is {}", 42);
  sub rsp, 24
  mov qword ptr [rsp], 42
  lea rdi, [rip + .L.str]
  mov rcx, rsp
  mov esi, 18
  mov edx, 1
  call fmt::v10::vprint(fmt::v10::basic_string_view, fmt::v10::basic_format_args >)@PLT
  add rsp, 24
  ret

Да, код, генерируемый std::printf продолжает оставаться самым маленьким по размеру, однако давайте рассмотрим бенчмарк, сравнивающий эталонную реализацию print, предоставляемую библиотекой libfmt, c printf и ostream:

#include 
#include 

#include 
#include 

void printf(benchmark::State& s) {
  while (s.KeepRunning())
    std::printf("The answer is %d.\n", 42);
}
BENCHMARK(printf);

void ostream(benchmark::State& s) {
  std::ios::sync_with_stdio(false);
  while (s.KeepRunning())
    std::cout << "The answer is " << 42 << ".\n";
}
BENCHMARK(ostream);

void print(benchmark::State& s) {
  while (s.KeepRunning())
    fmt::print("The answer is {}.\n", 42);
}
BENCHMARK(print);

void print_cout(benchmark::State& s) {
  std::ios::sync_with_stdio(false);
  while (s.KeepRunning())
    fmt::print(std::cout, "The answer is {}.\n", 42);
}
BENCHMARK(print_cout);

void print_cout_sync(benchmark::State& s) {
  std::ios::sync_with_stdio(true);
  while (s.KeepRunning())
    fmt::print(std::cout, "The answer is {}.\n", 42);
}
BENCHMARK(print_cout_sync);

BENCHMARK_MAIN();

При компиляции с помощью apple clang 11.0.0 c флагами -O3 -DNDEBUG и запуске на MacOS 10.15.4 мы получаем следующие результаты:

Run on (8 X 2800 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 262K (x4)
  L3 Unified 8388K (x1)
Load Average: 1.83, 1.88, 1.82
-----------------------------------------------------------
Benchmark                Time             CPU   Iterations
-----------------------------------------------------------
printf                87.0 ns         86.9 ns      7834009
ostream                255 ns          255 ns      2746434
print                 78.4 ns         78.3 ns      9095989
print_cout            89.4 ns         89.4 ns      7702973
print_cout_sync       91.5 ns         91.4 ns      7903889

print оказывается лидером: мало того, что код, генерируемый им, оказывается в несколько раз меньше, чем код, генерируемый потоками, так он еще при выводе в stdout оказывается на 14% быстрее, чем printf (который также по умолчанию выполняет вывод в stdout).

Как оптимально и кроссплатформенно вывести форматированный текст на экран в C++? Используйте std::print!

Незначительные нюансы

Правда, если вы используете отличные от Windows системы, вам сперва придется дождаться поддержки std::print в libc++ и libstdc++. По состоянию на 17.07.2023, он не поддерживается ни там, ни там.

Если же вам не хочется ждать, вы всегда можете использовать fmtlib.

© Habrahabr.ru