Рассмотрим следующий пример:
#include <format>
#include <print>
int main() {
int x = 12345;
// note: this is "awoo" followed by 28 zeros ([dcl.init.string] p3)
char buffer[32] = "awoo";
std::format_to(buffer, "{}{}", x, buffer);
std::println("{}", buffer);
}
Ожидается, что этот код выведет 12345awoo
, потому что мы форматируем {}{}
с аргументами 12345
и "awoo"
.
Однако фактический результат равен 1234512345
. Причина этого очевидна: форматирование 12345
будет записывать в буфер, и к тому времени, когда buffer
(второй аргумент) будет обработан std::format_to
, он больше не будет содержать "awoo"
.
Возникает вопрос: каков ожидаемый результат согласно стандарту C++? https://eel.is/c++draft/format.functions#lib:vformat_to не указывает в качестве предварительного условия, что псевдонимы между аргументами и выходным буфером не разрешены, поэтому я не верю в это в этом случае поведение неопределенное.
@OldBoy, у тебя есть стандартная цитата, подтверждающая это? Это, конечно, верно для некоторых функций, таких как std::memcpy
, но это необходимо указать в предусловиях функции. Это не так, если не указано иное.
Ожидаемый результат (из коробки, порядок оценки) — 1234512345
. Еще интереснее, есть ли у Клинта и подобные замечания вещи. Интересно, требуется ли оценка to_format
для добавления завершающего \0
при заполнении buffer
. Когда нет, эффект мог бы быть таким "1234512345hu#+hi-.dfhd"
@JoopEggen to_format
принимает только выходной итератор и не знает остальную часть буфера, поэтому он не может писать дальше конца 1234512345
без риска сбоя сегментации. Как вы думаете, почему ожидаемый результат равен 1234512345
? Есть ли какая-то гарантия, что входные аргументы обрабатываются один за другим, слева направо, и одновременно продвигается выходной итератор?
@JoopEggen, в \0
куча buffer
, поэтому to_format
добавлять не нужно
buffer
мог содержать "awoo\0u#+hi-.dfhd\0..."
(32 места или ошибка сегмента), тогда первый аргумент int дает "12345u#+hi-.dfhd\0..."
с выходным итератором с индексом 5, а затем окончательный результат.
@JoopEggen мы на одной волне? В другом примере да, гипотетически может быть awoo\0xzy...
, но в этом примере все после awoo
инициализируется нулями (см. eel.is/c++draft/dcl.init.string#3).
@Caleth buffer
обычно не заполняется \0
(если в спецификации не было внесено изменений). Компилятор Microsoft имел привычку заполнять \0
s
Хорошо, я должен извиниться перед некоторыми людьми. @ЯнШультке
Это не указано, оба 1234512345
и 12345awoo
соответствуют результатам.
Функции, определенные в [format.functions], используют специализации средства форматирования шаблонов классов для форматирования отдельных аргументов.
Таким образом, форматирование каждого аргумента происходит внутри отдельного вызова функции для остальных. Нет никаких требований к порядку этих вызовов, поэтому побочные эффекты неопределенно последовательны друг с другом.
Это довольно натянуто, но даже разумные реализации могут привести к неопределенному поведению 12345awoo
. Не исключено, что супероптимизированная реализация, получив std::uint8_t
, «отформатирует» его через таблицу поиска, загрузив результат в регистр, чтобы не нужно было немедленно записывать его в буфер. Если бы он распознал, что awoo
— короткая строка, он, возможно, также мог бы загрузить ее в регистр, объединить две строки в регистре xmm с помощью побитового сдвига, а затем записать все, используя одну инструкцию mov mem, xmm0
.
Почти уверен, что вызовы должны выполняться по порядку... откуда вы можете знать, где форматировать N+1
-й аргумент, если вы не знаете, сколько байтов заняли первые N
-аргументы (чего вы, вообще-то, не знаете) )?
@Барри, да, посмотри историю редактирования этого ответа. В большинстве случаев вы этого не делаете, но могут возникнуть ситуации, когда умный компилятор может сэкономить несколько циклов, выполнив все по-другому.
@Caleth Умный компилятор не может. Умная библиотека... потенциально может быть в некоторых очень узких ситуациях с неясной выгодой?
@Барри, извини, умная реализация может
@Барри, тебе вроде как нужно знать, сколько символов приняли предыдущие аргументы, но это не значит, что тебе нужно немедленно записывать все в выходной буфер. При форматировании группы std::uint8_t
вы также можете буферизовать отформатированные байты в регистрах, объединить их в регистр xmm
и выполнить одиночный mov mem, xmm0
в самом конце. Другими словами, стандарт не налагает никаких требований относительно того, когда выходной итератор продвигается вперед и как это происходит с точки зрения форматирования аргументов. Реализация может использовать это, особенно если этот итератор непрерывен.
Теперь это LWG Issue 4078.
Хотя результат в настоящее время не указан: 1234512345
или 12345awoo
(как объяснено в ответе @Caleth), этот случай также не должен работать; то есть это должно быть неопределенное поведение.
Подобные функции, такие как sprintf, имеют параметр char *restrict buffer
для вывода; они не поддерживают псевдонимы между аргументами и выходным буфером. Пользователю неразумно ожидать, что это будет работать, и неразумно, чтобы реализация поддерживала этот вариант использования.
В какой-то момент std::format_to(buffer, "{}{}", x, buffer);
, скорее всего, станет неопределённым поведением, однако это может занять много времени, поскольку сложно придумать формулировку, запрещающую этот вариант использования, и это не является проблемой высокого приоритета.
Использование одного и того же буфера в качестве источника ввода и назначения вывода считается неопределенным поведением.