Рано или поздно у каждого программиста появляется желание вывести форматированный текст на экран. Немудрено, у пляшущих на экране буковок есть свой неповторимый шарм, а факт форматированности добавляет им еще и загадочности — мы можем даже не догадываться, что в точности будет напечатано.
Но как сделать это оптимально и кроссплатформенно?
Стойте, стойте, но у нас ведь все для этого есть
Казалось бы, если мы желаем вывести форматированный текст на экран, в 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.
Комментарии (10)
SpiderEkb
17.07.2023 11:45-1Вы меня, конечно, извините, но
fmt::print(std::cout, "The answer is {}.\n", 42);
Это не вывод форматированного текста. Форматированный текс выглядит как у вас показано:
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
Т.е. кроме содержимого необходимо еще указывать формат для каждого поля - выравнивание, размер поля, чем заполнять и т.п.
Строго говоря, оно даже вот так должно выглядеть:
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.0 ns 255.0 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
Т.е. целое число 255 должно "добиться" до 255.0 и выровняться по правому краю. А все флоаты должны округлится (или хотя бы обрезаться) до указанного количества десятичных знаков. И тоже выровняться.
Вот это будет вывод форматированного текста.
В целом, все это есть в printf. Но там с локалями проблемы.
Если это есть в srd::format или std::print - ну так и надо все это расписать.
В противном случае заголовок статьи не соответствует содержанию.
cher-nov
17.07.2023 11:45-1Вы серьёзно утверждаете, что разбор форматной строки в run-time предпочтительнее compile-time?
eoanermine Автор
17.07.2023 11:45+2Только при использовании
std::format
,std::print
, ... разбор форматной строки, если я правильно понял, что вы имеете ввиду, происходит в compile-time (см. std::basic_format_string)cher-nov
17.07.2023 11:45-1А, хорошо. Я не специалист в Modern C++, увы. Просто меня смутил вот этот пассаж в статье:
Ведь у нас появился
std::print
, <...> не менее эффективный, чемstd::printf
И при этом ни одного упоминания о разнице между временем исполнения, хотя это едва ли не главная причина, почему вывод текста через
std::cout
вообще изначально появился.
CoolCmd
17.07.2023 11:45+1кстати, по поводу размера. в visual c++ вызов
std::format()
одним махом увеличивает размер exe-файла на 200 KB. и в DLL со стандартной С++ библиотекой эта шаблонная магия лезть отказывается, в отличие отstd::printf()
.std::print()
пока не завезли, но по идее размер уменьшится не должен.
buldo
17.07.2023 11:45А что по поводу кросс платформенных переносом строк? Как это красиво делать на C++?
kekoz
17.07.2023 11:45+1Дочитал до этого места.
Если на большинстве Linux и MacOS систем вышеприведенный код выведет ровно то, что мы от него и ожидаем, то на Windows мы обречены увидеть кракозябры, например:
Привет, κόσμος!
, как бы мы этого не пытались избежать различными флагами компиляции.Автор, ЧЯДНТ?
Если в любой unicode-aware OS (и Windows входит в их число начиная примерно с NT 3.1) от этой программы вы увидите “кракозябры”, то проблема совершенно точно не в ключах компиляции...
Quarc
17.07.2023 11:45+1Исходник в UTF-8, консоль в CP866, "кракозябры" обязаны быть. И проблема определенно не в ключах компиляции, и вообще не имеет отношения ни к компилятору, ни к языку.
lrrr11
я согласен, что с привычными способами вывода мягко говоря не все так (кстати еще можно скомпилировать
с
-static-libstdc++
и посмотреть на размер получившегося бинарника)Однако и
std::print
не оптимальное решение проблемы. Советую посмотреть хотя бы на https://github.com/cppfastio/fast_io. А в общем случае видимо придется иметь дело с direct io, uring и вот этим всем.eoanermine Автор
Спасибо, очень интересно было почитать документацию к fast_io. Однако это едва-ли можно назвать general purpose решением: прикручивать форматирование собственных классов в эту библиотеку, видится, очень больно (например, вот так реализуется поддержка std::complex).