Рано или поздно у каждого программиста появляется желание вывести форматированный текст на экран. Немудрено, у пляшущих на экране буковок есть свой неповторимый шарм, а факт форматированности добавляет им еще и загадочности — мы можем даже не догадываться, что в точности будет напечатано.
Но как сделать это оптимально и кроссплатформенно?
Стойте, стойте, но у нас ведь все для этого есть
Казалось бы, если мы желаем вывести форматированный текст на экран, в 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<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)@PLT
mov rdi, rbx
mov esi, 42
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@PLT
lea rsi, [rip + .L.str.2]
mov edx, 2
mov rdi, rax
pop rbx
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, 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<char>, fmt::v10::basic_format_args<fmt::v10::basic_format_context<fmt::v10::appender, char> >)@PLT
add rsp, 24
ret
Да, код, генерируемый std::printf
продолжает оставаться самым маленьким по размеру, однако давайте рассмотрим бенчмарк, сравнивающий эталонную реализацию print
, предоставляемую библиотекой libfmt, c printf
и ostream
:
#include <cstdio>
#include <iostream>
#include <benchmark/benchmark.h>
#include <fmt/ostream.h>
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.