Тем, что в URL можно указать параметры печати pdf-файла: dpi (разрешение), двухсторонняя печать и прочее.
Можно отследить прогресс печати (очень полезно, если pdf-файл состоит из нескольких сотен страниц, например).
Более того, зайдя в браузере на веб-страницу http://127.0.0.1:950 можно посмотреть состояние принтера и установить какие-нибудь настройки по умолчанию.
Считаю, что можно подвести итоги голосования/опроса.
В первую очередь, на что можно обратить внимание — это неверное понимание первого вопроса голосования «Как с помощью std::chrono получить наиболее точное текущее значение системного времени?». Слова «наиболее точное» в нём имеют второстепенное значение, а первостепенно то, что необходимо получить значение системного времени. Имеется в виду время, которому посвящена соответствующая статья в Википедии — System time. Хотя однозначного определения у него нет (точка отсчёта и единицы измерения могут отличаться в различных реализациях), речь идёт о представлении календарного времени в компьютерных системах. Большинство же проголосовавших выбрали вариант «std::chrono::high_resolution_clock::now()». Эта функция возвращает не системное время, а количество тиков таймера/часов в системе с наибольшим разрешением. При этом отсчёт такого времени не привязан ни к какой календарной дате и в некоторых реализациях (в т.ч. в MSVC) отсчёт high_resolution_clock ведётся от момента запуска операционной системы.
Ну ладно, отложим вопрос терминологии в сторону и посмотрим, сколько участников голосования выбрали несуществующие функции. За system_time(), system_time::current(), system_time::now(), system_clock::time() и clock::system_time() суммарно проголосовало 21 человек, в то время как за правильный ответ — system_clock::now() — всего 18. С возвращаемым типом ситуация чуть получше: несуществующие типы выбрали 12 человек, а правильный ответ — system_clock::time_point — 16. На вопрос «Сколько байт занимает этот тип?» неверно ответили 47 человек из 89 проголосовавших [53%]. А на вопрос «Что означает этот тип...?» — 49 из 91 [54%].
О чём это может говорить?
О том, что комитет по стандартизации C++ плохо выполняет свою работу и возникает ощущение, что члены комитета не понимают, в чём вообще заключается их работа.
Вместо того, чтобы взять и разобраться с тем, как получается время в различных наиболее распространённых операционных системах (в каких единицах измеряется, откуда ведётся отсчёт, примерная скорость работы/производительность различных функций получения времени), предоставив C++ разработчикам простую и удобную кроссплатформенную тонкую обёртку над системными функциями получения времени, в комитете C++ наплодили кучу лишних сущностей (std::chrono::duration, std::chrono::duration_cast, std::chrono::time_point_cast, std::ratio) и дали функциям получения времени такие мудрёные названия, которые невозможно запомнить.
А можно было поступить так:
Не мудрить с std::ratio и функцией получения текущего системного времени возвращать всегда количество наносекунд с начала эпохи. Т.к. POSIX-функция clock_gettime() возвращает время в наносекундах, а наиболее точное системное время в Windows возвращается функцией GetSystemTimePreciseAsFileTime() в 100-наносекундных интервалах, то получается, что одна наносекунда является общим знаменателем.
Предоставить функцию, возвращающую точность измерения времени (в POSIX точность можно получить через вызов clock_getres(), в Windows возвращать константу 100).
Частоту таймера высокого разрешения сделать зависящей от реализации константой времени выполнения (в std::chrono частота/период таймера является константой времени компиляции, что плохо согласуется с реализацией таймера высокого разрешения в Windows, где частота возвращается системной функцией QueryPerformanceFrequency()).
Получение системного времени в распространённых операционных системах возможно в двух вариантах: менее точное время [clock_gettime(CLOCK_REALTIME_COARSE) в POSIX и GetSystemTimeAsFileTime() в Windows] и более точное ценой больших вычислительных затрат на его получение [clock_gettime(CLOCK_REALTIME) в POSIX и GetSystemTimePreciseAsFileTime() в Windows]. Поэтому функций получения системного времени должно быть две: быстрое менее точное и более точное. [Либо одна функция, но с дополнительным аргументом.]
В текущей же спецификации std::chrono получилась такая ситуация, что есть ненужный функционал, а функционала нужного нету. И если вернуться к вопросу голосования «Как с помощью std::chrono получить наиболее точное текущее значение системного времени?», то вообще говоря правильный ответ будет — никак. :)(: Т.к. системное время в std::chrono представляет единственный класс std::chrono::system_clock. И в std::chrono нельзя получить менее точное системное время и более точное, а можно получить только просто какое-то системное время. А какое именно — определяет реализация (в той же Windows это может быть GetSystemTimePreciseAsFileTime(), а может быть GetSystemTimeAsFileTime()).
Отличный пример отвратительной реализации чего-то что этим не является.
Послушайте. Текущее решение — это не первое, что пришло мне в голову, и что я сразу же бросился реализовывать. И это не первая реализация типа File в 11l. Это результат размышлений на протяжении нескольких лет.
И принимая данное решение я прежде всего исходил из соображений практичности. С задачами типа «его хэндл/дескриптор можно передать в другой процесс» типичный программист прикладного ПО за всю свою карьеру сталкивается примерно ни разу. Признайтесь честно, ведь и вам не приходилось сталкиваться с такой задачей на практике?
К тому же, тип File в 11l — это аналог std::fstream (а точнее, std::ifstream/std::ofstream) в C++. Вас же не смущает невозможность передать std::fstream в другой процесс?
Для обёртки вокруг низкоуровневого файлового дескриптора будет использоваться отдельный тип (вроде FileDescriptor/FileHandle или даже os:File), когда он понадобится. А короткое имя File гораздо больше смысла имеет оставить за реализацией, которая и требуется чаще всего при типичной работе с файлами (прочитать или записать что-то в файл).
Всего-то надо позвать функцию ОС и сделать это удобно.
Если файл читается/пишется не целиком, то «всего-то позвать функцию ОС» не достаточно, т.к. вызов функции ОС осуществляет переключение в Ring 0 и обратно, что очень дорого. Для эффективной работы без буферизации чтения/записи не обойтись.
обычный булевый флаг-параметр, который совсем не всегда константа
Если такое и потребуется, то лучше использовать отдельный тип для этого (IOFile например). Тип File в 11l ориентирован на типичную работу с файлами, причём эффективную (за счёт буферизации) и безопасную (за счёт проверок на этапе компиляции).
IFile это очень плохое имя, даже с учетом всех рассуждений в комментарии
Просто я фанат предельной краткости, но в данном случае, видимо, это обернулось против меня.
и почему не используется отдельный namespace?
Да, наверное, так и следует сделать, но я пока что не определился с названием этого namespace (всё-таки ffh — это неформальное название библиотеки, и мне не очень хочется его фиксировать в исходном коде).
функции "прочитай и верни строку по значению" в стандартном API нет из соображений производительности, при работе в цикле данные вычитываются в "разогретую" и преалоцированную область памяти
Это проблема языка программирования, которую совсем не обязательно перекладывать на программиста (жертвовать красотой API в угоду производительности). К примеру, 11l прекрасно оптимизирует запись line = f.read_line() в аналог std::getline(f, line);. [Транспайлер 11l → C++ уже поддерживает эту оптимизацию, но только для записи в форме <str_var> = <file_obj>.read_line(), а запись var <new_str_var> = <file_obj>.read_line() внутри цикла будет оптимизироваться в 11lc.]
И, к слову, проблема с именем типов для файловых объектов в языке 11l решилась очень оригинальным образом: в исходном коде тип всегда называется просто File [также как в языках Ruby, Java/Kotlin, D], но фактический тип определяется на основе режима открытия файла: если режим не указан, то файл открывается для чтения и имеет тип FileForReading, а если указан режим WRITE или APPEND, то файл имеет тип FileForWriting. Соответственно, набор доступных методов в фактическом типе отличается и вызвать write() у открытого для чтения файла будет ошибкой компиляции. А если File используется в качестве имени типа аргумента функции, то фактически используется объединение типов FileForReading|FileForWriting|FileForReadingAndWriting. Причём, фактически используемые имена типов файловых объектов (например, FileForWriting) в исходном коде на 11l на данный момент вообще недоступны, а доступен только псевдотип File.
P.S.
Кстати говоря, насчёт оптимизации записи line = f.read_line() в f.read_line(line). Чисто теоретически, насколько я понимаю, такую оптимизацию можно было бы протащить и в C++! Дело в том, что в C++ есть такая штука, как "as-if rule". Это правило, которое разрешает любые преобразования кода, которые не изменяют наблюдаемое поведение программы. As-if rule, к примеру, разрешает C++ компилятору встраивать любые функции, даже те, которые не имеют пометки inline. А здесь говорится о возможности замены типа аргумента функции const T& на const T компилятором в том случае, когда T является простым типом, например int. Однако, в результате замены line = f.read_line() на f.read_line(line) паттерн работы со строкой line будет отличаться [отличие заключается в "разогреве" строки]. Поэтому, если печатать в цикле значение line.capacity(), то компилятор не сможет применить "as-if rule", т.к. оптимизация в данном случае изменит наблюдаемое поведение программы. Однако, если обращений к line.capacity() в коде нет, и если глобальный оператор new не переопределён [в случае данной оптимизации оператор new будет вызываться гораздо реже], то наблюдаемое поведение программы не поменяется и C++ компилятор вполне может применить такую оптимизацию!
Не вижу причин, по которым ваш ~OFile не может быть реализован вот так:
~OFile() { close(); }
Такое тоже будет работать, но что это даёт? Просто будут выполняться некоторые лишние действия (fh.close(); вызовется повторно в деструкторе fh, а buffer_pos = 0; хотя ничего и не испортит, но избыточно).
Следовательно, в условном Traits::destroy вы можете делать флуширование, если вам это нужно.
Чтобы делать флуширование, необходим доступ к buffer и buffer_pos объекта OFile. Т.е. в ваш handle_holder помимо самого handle, необходимо добавить указатель на объект OFile, std::function или что-то вроде того.
Да уже понятно что у вас пунктик по этому поводу.
Вы так говорите, как будто это что-то плохое. :)(: А вот что действительно печально, так это то, что конструктивной дискуссии у нас совсем не получается. Я предлагаю [не только вам, а вообще всем читателям] простое решение вполне конкретной проблемы. Вы же всеми силами пытаетесь придумать, почему такое решение неправильно, и даже не хотите признавать наличие проблемы как таковой (проблемы очистки ресурсов в move assignment operator).
если использовать идиому "make temporary then swap", то модифицировать придется реализацию swap
А если использовать move_assign(), то ничего модифицировать не придётся. А функция swap() будет генерироваться автоматически на основе move constructor и move assignment operator. Да, получится чуток менее эффективно ручной реализации swap(), но... если честно, для чего может потребоваться swap() у объектов вроде OFile для меня остаётся загадкой.
Только вот если вашу реализацию использовать в рамках C++17, то там нужен std::launder
Для заменяемых типов (transparently replaceable) — не нужен. (Т.к. в данном случае имеет место реконструкция объекта того же самого типа, т.е. тип исходного объекта и вновь созданного полностью совпадают.) Но давайте не будем спорить на тему того, в чём мы детально не разбираемся. :)(:
Или у вас есть конкретный пример кода использования move_assign(), который некорректно работает без этого std::launder?
а возвращенное оператором placement new значение выбрасываете.
А куда его девать, это возвращённое значение? Мне вообще-то в данном случае нужен просто вызов move constructor-а. И мой код основан на реализации std::allocator<T>::construct(). Почему явный вызов конструктора в C++ записывается через placement new — вопрос не ко мне. Если можно вызвать move constructor как-то более правильно — дайте знать как.
А если он может делать close, то очистка ресурсов в move assignment operator не проблема.
Проблема. Т.к. одного умения делать close, увы, не всегда достаточно. Конкретный пример: деструктор в классе OFile вызывает метод flush(), который записывает все оставшиеся данные в буфере в файл. [Больше ничего деструктор класса OFile не делает, т.к. handle закрывается в деструкторе класса detail::FileHandle (сразу после выполнения кода деструктора OFile).]
Если взять вашу реализацию handle_holder, то толку от наличия TRAITS::destroy() будет немного, т.к. перед закрытием handle необходимо вызвать flush() у объекта-владельца handle, и в итоге move assignment operator для класса OFile реализовывать придётся как-то так:
OFile &operator=(OFile &&f)
{
flush(); // из-за этого вызова приходится определять `operator=(OFile &&)`
fh = std::move(f.fh);
buffer = std::move(f.buffer);
buffer_pos = f.buffer_pos;
buffer_capacity = f.buffer_capacity;
return *this;
}
С помощью функции move_assign() можно, во-первых, существенно сократить код реализации operator=(OFile &&) [вот эта реализация] и, во-вторых, что более важно: при добавлении новых переменных-класса в OFile обновлять код реализации operator=(OFile &&) не потребуется.
Так что мой вопрос остаётся в силе: было бы интересно увидеть, какую альтернативу моему решению предлагаете вы? [Уж случаем не идиому ли "make temporary then swap"? :)(:]
надо договориться о том, какие у нас "правильные" ожидания.
Так ведь давно уже все договорились.
Ответы: 0, 1, 1, 1, 2.
Код чтения файла по строкам для языков Python, C++ и Rust я уже приводил в этом комментарии. Только что проверил в Nim [взяв за основу этот код, а также используя lines] и в Go [на основе кода отсюда]. Результат такой же.
Отличие в разных языках только в том, включается ли в прочитанную строку символ \n или нет (и функция read_line() в ffh имеет опциональный аргумент keep_newline для выбора желаемого поведения).
Изначальный посыл статьи был в неочевидной работе метода eof() в C++. И неправильное количество прочитанных строк получалось именно из-за него.
Вот здесь не все возвраты -1 будут ошибкой. Если в errno находится EINTR, то такую ситуацию надо обрабатывать как частичное чтение
Насколько я понимаю, для обычных файлов такого произойти не может. Это только для "slow" devices (terminal, pipe, socket). И ни в одной реализации стандартной библиотеки языка Си (для Linux и FreeBSD) я не увидел проверки на EINTR в коде fread/refill: везде в случае, когда read возвращает число < 0, сразу возвращается ошибка. Поправьте, если ошибаюсь.
которое не успело прочитать совсем ничего (0 байт).
Возврат 0 — это ведь всегда признак конца файла, а не "прочитано 0 байт". (Т.е. функция read никак не может проинформировать о том, что было успешно прочитано 0 байт.)
Когда вы читаете из файла, вы передвигаете (модифицируете) текущую позицию в файле, поэтому доступ из разных потоков должен быть синхронизирован.
Когда читаете файл последовательно сразу из нескольких параллельных потоков?
Как я уже говорил выше, это ультра редкий кейс, для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%). [И если такое и правда нужно — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.]
В POSIX есть unlocked версии некоторых функций работы с файлами, например getc_unlocked, также есть функции ручной блокировки flockfile
Вы так говорите, как будто их кто-то использует. :)(: Я вот узнал об этих функциях только в процессе написания данной статьи. Программисты хотят решать прикладные задачи, а не погружаться в детали реализации каждой библиотечной функции, которую они используют в своём коде. К тому же, в файловых потоках C++ unlocked версий нет.
Так ведь можно вообще не иметь move_assign и делать просто *dest = std::move(other) в местах, где вы применяете move_assign.
Чтобы делать «просто *dest = std::move(other)» кто-то этот оператор присваивания перемещением должен реализовать: либо программист, либо компилятор. И функция move_assign() нужна для упрощения реализации этого оператора.
Но мотивация к появлению move_assign не смотря на обилие текста от меня ускользнула.
Понять, для чего нужен move_assign(), можно только на практике. Если у вас есть реализованный move assignment operator, то я могу показать, как его код можно упростить с помощью функции move_assign().
Но, за неимением вашего кода, давайте покажу на примере кода от Microsoft.
if (this != &other)
{
// Free the existing resource.
delete[] _data;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
Все эти строки можно заменить на одну строку move_assign(this, std::move(other));.
Нафиг нужен UniqueHandle, который не умеет закрывать хранящийся в нем дескриптор.
Ну я же привёл ссылку на коммит. Благодаря UniqueHandle и move_assign код класса FileHandle сократился на 10 строчек.
Но, что даже более важно, благодаря UniqueHandle можно добавлять в FileHandle новые переменные-класса только в одном месте — в самом классе, т.е. править код move assignment operator и move constructor при этом не нужно, т.к. новые переменные-класса будут учитываться автоматически в сгенерированном компилятором коде move assignment operator и move constructor.
Если вы считаете, что UniqueHandle не нужен, то было бы интересно увидеть, какую альтернативу моему решению предлагаете вы (желательно не просто на словах, а в виде конкретного кода).
Может лучше пожалеть людей из комитета, которым придется читать подобный бред?
Весь proposal, по сути, это одно предложение из последнего абзаца, смысл которого заключается в том, чтобы C++ компилятор автоматически генерировал реализацию move assignment operator на основе деструктора и пользовательского move constructor. Это предложение не вводит никаких новых понятий (ни move_assign, ни UniqueHandle), а позволяет просто писать меньше кода, вот и всё.
Можно спросить, а почему такая мудреная реализация move_assign?
Ответ в виде мини-статьи
О, move_assign(), а также класс UniqueHandle — это моя новаторская технология. :)(:
Недавно я добавлял переменную-класса, у которого был move-конструктор. В класс-то я добавил, а вот move-конструктор поправить я забыл. Когда обнаружил ошибку, я задался вопросом: как бы сделать так, чтобы компилятор автоматически обновлял/корректировал move-конструктор при добавлении переменных-класса? Тут я вспомнил, что если явно move-конструктор не объявлять [либо объявлять без тела с пометкой = default;], то он генерируется компилятором автоматически. Хорошо, а что мешало в данном случае генерировать компилятору корректный move-конструктор [или, другими словами, почему не подходил сгенерированный компилятором move-конструктор, почему он некорректный]? Проблема оказалась в одной единственной переменной-класса типа HANDLE, которая в move-конструкторе инициализировалась значением из другого объекта, а в этом другом объекте устанавливалась в INVALID_HANDLE_VALUE (т.е. что-то вроде handle = other.handle; other.handle = INVALID_HANDLE_VALUE;).
«А что, если этот handle обернуть в некий вспомогательный класс?» — подумал я.
("Unique", т.к. он запрещает копирование, а позволяет только перемещать себя, по аналогии с std::unique_ptr.)
UniqueHandle<HandleType, HandleType default_value> пытается вести себя как handle типа HandleType: у него определены оператор неявного приведения к типу HandleType, а также оператор присваивания значения типа HandleType. Но в отличие от сырого handle, переменная-класса типа UniqueHandle умеет себя перемещать (кодом handle = other.handle; other.handle = default_value;) и автоматически инициализироваться значением default_value.
Так я избавился от объявления move-конструктора.
И т.к. это происходило во время разработки библиотеки ffh, я решил, что переменную handle/fd в классе FileHandle можно также заменить на UniqueHandle. Вот соответствующий коммит.
Далее встал вопрос — а что делать с оператором присваивания перемещением (move assignment operator)? Если посмотреть на код уже существующих операторов присваивания перемещением, то легко заметить, что сначала идёт освобождение ресурсов, а затем перемещение, причём код освобождения ресурсов в точности совпадает с кодом деструктора, а код перемещения совпадает с кодом move-конструктора.
Таким образом, реализацию оператора присваивания перемещением при наличии move-конструктора можно генерировать автоматически! Именно это и делает вспомогательная функция move_assign(): сначала вызывает деструктор (dest->~Ty();), а затем — move-конструктор (new(dest)Ty(std::move(other));).
К слову, Microsoft предлагает делать наоборот: «you can eliminate redundant code by writing the move constructor to call the move assignment operator». (Т.е. предлагает реализовывать move constructor как *this = std::move(other);.) Но такое решение хуже тем, что реализация move assignment operator сложнее реализации move constructor, а также тем, что при этом выполняются лишние действия: вначале конструируется пустой объект, который сразу же будет разрушен внутри реализации move assignment operator (т.е. выполняется цепочка «конструктор-по-умолчанию—деструктор—move-конструктор»). Поэтому лучше выражать move assignment operator через move constructor, а не наоборот.
Почему бы не реализовать в классе UniqueHandle ещё и move assignment operator? Потому, что UniqueHandle не умеет корректно закрывать handle. И даже если его научить (посредством дополнительного шаблонного параметра — класса, у которого определена статическая функция close(), которая закрывает handle), то всё равно иногда требуется произвести какие-то дополнительные действия перед тем, как закрывать handle, например flush — записать оставшиеся в буфере данные в файл, handle которого находится в данном UniqueHandle. (Т.е. иногда нужны такие дополнительные действия перед закрытием handle, которые никак не получится сделать на уровне UniqueHandle.)
На данный момент C++ компилятор автоматически добавляет удалённый [т.е. помеченный как = delete;] оператор присваивания в случае, когда в классе есть определяемый пользователем конструктор перемещения (а определяемого пользователем оператора присваивания нет). Но я считаю, что компилятор C++ в этом случае может автоматически генерировать реализацию move assignment operator на основе деструктора и пользовательского конструктора перемещения, таким образом делая в точности то, что делает функция move_assign(). Возможно, если не поленюсь, напишу proposal к стандарту C++. :)(:
Почему нельзя было просто сделать *dest = std::move(other)?
Если функцию move_assign() "реализовать" как *dest = std::move(other);, то при выполнении кода detail::FileHandle<true> fh; fh = detail::FileHandle<true>(); получится бесконечная рекурсия и, как следствие, переполнение стека. Т.к. FileHandle::operator=(FileHandle &&fh) реализован как move_assign(this, std::move(fh));.
Да, его, конечно, можно отнести к «множеству существующих в настоящее время HDD», но в статье речь была об «используемых в настоящее время». Вы правда используете его по назначению в 2024 году?
несколько потоков читают из-одного файлового дескриптора (странный кейс, но в целом возможный и валидный)
Это ультра редкий кейс [если мы говорим про последовательное буферизованное чтение файла, а не выборочное чтение блоками из разных мест файла по заданному смещению], для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%).
Обычно всё-таки каждый поток использует свой отдельный дескриптор файла, а если прям нужно параллельное чтение из одного дескриптора — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.
Меня больше задевает обработка ошибок исключительно через исключения.
В современных компиляторах исключения создают совсем небольшой оверхед (в том случае, когда они срабатывают только в исключительных случаях, т.е. обычно не срабатывают вообще).
Я провёл тесты с опциями компилятора /EHs-c- /D_HAS_EXCEPTIONS=0, дополнительно заменив в исходном коде ffh все throw на abort(), и не обнаружил разницы в производительности чтения посредством библиотеки ffh. Однако чтение посредством std::ifstream благодаря отключению исключений ускорилось процентов на 20-30. (Хотя, похоже, это связано не с самими исключениями, а с изменением логики работы при отключении исключений.)
Как вы думаете, как работает print? Почему по вашему, две команды print выводят две строки, а не два символа в одну строку?
Да, в отличие от простого sys.stdout.write() у функции print() есть дополнительный аргумент end, который по умолчанию равен символу новой строки \n. Упрощённо, реализацию print() с одним аргументом в Python можно записать так:
def print(arg, end = "\n"):
sys.stdout.write(str(arg))
sys.stdout.write(end)
Более подробно, как работает print() в реализации Python можно посмотреть здесь.
при печати одного символа, печатается 3 байта. Сам байт, возврат каретки и новая строка.
Так происходит только в Windows. Причём на уровне записи в текстовый файл. Хотя фактически текстовые файлы с разделителями строк в стиле Unix (одиночный \n, код символа — 10) уже давно поддерживаются всеми программами под Windows. Даже встроенный в Windows 10 блокнот научился нормально открывать такие файлы. И то, что при сохранении текстовых файлов Блокнот или Microsoft Word разделяют строки парой символов \r\n, это исключительно по религиозным соображениям — в Microsoft не хотят признавать, что это не имеет смысла.
Ваш код будет выводить лишнюю пустую строку в конце. Если файл f.txt состоит всего из одной строки (причём даже не важно, есть ли символ новой строки в конце файла или нет), то ваш код вызовет функцию print()два раза, хотя строка в файле одна.
А мануал почитать на туже readline у Python?
Очень похоже, что вы взяли этот Python-код из какой-то левой статьи, вроде такой. В мануале такого бреда не встретишь.
Если и читать файл посредством readline(), то код обычно используют такой (на основе примера отсюда):
with open("myfile.txt") as fp:
while True:
line = fp.readline()
if line == "":
break
print(line.rstrip())
На всех современных устройствах, включая nvme диски обычно используется размер сектора в 512 байт.
Эти устройства лишь сообщают о том, что размер сектора у них 512 байт. Но физически размер сектора уже везде 4096 байт и более. Вы разве не слышали про 512e?
Сектора - это про адресуемые единицы на устройстве.
Именно. Т.е. когда вы запрашиваете у устройства 512 байт, то фактически будет читаться 4096 байт и более. Поэтому запрашивать меньше 4096 байт не имеет смысла.
Все мои HDD тоже имеют 512, хотя они старые
Укажите номера моделей ваших HDD. Очень сомневаюсь, что там 512n.
К примеру, в моём ноутбуке 2014 года был установлен HDD ST500LT012-1DG142 на 500 Гб, который я уже давно заменил на SSD. Так вот, в спецификации от Seagate написано: Bytes per sector 512 (logical) / 4096 (physical)
А почему при чтении используется readline(), вместо потокового ввода?
Вы имеете в виду getline()?
Это же c++? Логично сразу учить студентов корректно работать с потоковым вводом.
А на что вы предлагаете заменить getline()?
Т.к. строки в файле идут в виде <слово> - <перевод>, то при чтении строки parser - синтаксический анализатор посредством кода f >> s прочтётся 4 строки: parser, -, синтаксический и анализатор.
Тем, что в URL можно указать параметры печати pdf-файла: dpi (разрешение), двухсторонняя печать и прочее.
Можно отследить прогресс печати (очень полезно, если pdf-файл состоит из нескольких сотен страниц, например).
Более того, зайдя в браузере на веб-страницу
http://127.0.0.1:950
можно посмотреть состояние принтера и установить какие-нибудь настройки по умолчанию.Считаю, что можно подвести итоги голосования/опроса.
В первую очередь, на что можно обратить внимание — это неверное понимание первого вопроса голосования «Как с помощью std::chrono получить наиболее точное текущее значение системного времени?». Слова «наиболее точное» в нём имеют второстепенное значение, а первостепенно то, что необходимо получить значение системного времени. Имеется в виду время, которому посвящена соответствующая статья в Википедии — System time. Хотя однозначного определения у него нет (точка отсчёта и единицы измерения могут отличаться в различных реализациях), речь идёт о представлении календарного времени в компьютерных системах.
Большинство же проголосовавших выбрали вариант «std::chrono::high_resolution_clock::now()». Эта функция возвращает не системное время, а количество тиков таймера/часов в системе с наибольшим разрешением. При этом отсчёт такого времени не привязан ни к какой календарной дате и в некоторых реализациях (в т.ч. в MSVC) отсчёт high_resolution_clock ведётся от момента запуска операционной системы.
Ну ладно, отложим вопрос терминологии в сторону и посмотрим, сколько участников голосования выбрали несуществующие функции. За
system_time()
,system_time::current()
,system_time::now()
,system_clock::time()
иclock::system_time()
суммарно проголосовало 21 человек, в то время как за правильный ответ —system_clock::now()
— всего 18. С возвращаемым типом ситуация чуть получше: несуществующие типы выбрали 12 человек, а правильный ответ —system_clock::time_point
— 16. На вопрос «Сколько байт занимает этот тип?» неверно ответили 47 человек из 89 проголосовавших [53%]. А на вопрос «Что означает этот тип...?» — 49 из 91 [54%].О чём это может говорить?
О том, что комитет по стандартизации C++ плохо выполняет свою работу и возникает ощущение, что члены комитета не понимают, в чём вообще заключается их работа.
Вместо того, чтобы взять и разобраться с тем, как получается время в различных наиболее распространённых операционных системах (в каких единицах измеряется, откуда ведётся отсчёт, примерная скорость работы/производительность различных функций получения времени), предоставив C++ разработчикам простую и удобную кроссплатформенную тонкую обёртку над системными функциями получения времени, в комитете C++ наплодили кучу лишних сущностей (
std::chrono::duration
,std::chrono::duration_cast
,std::chrono::time_point_cast
,std::ratio
) и дали функциям получения времени такие мудрёные названия, которые невозможно запомнить.А можно было поступить так:
Не мудрить с
std::ratio
и функцией получения текущего системного времени возвращать всегда количество наносекунд с начала эпохи. Т.к. POSIX-функцияclock_gettime()
возвращает время в наносекундах, а наиболее точное системное время в Windows возвращается функциейGetSystemTimePreciseAsFileTime()
в 100-наносекундных интервалах, то получается, что одна наносекунда является общим знаменателем.Предоставить функцию, возвращающую точность измерения времени (в POSIX точность можно получить через вызов
clock_getres()
, в Windows возвращать константу 100).Частоту таймера высокого разрешения сделать зависящей от реализации константой времени выполнения (в std::chrono частота/период таймера является константой времени компиляции, что плохо согласуется с реализацией таймера высокого разрешения в Windows, где частота возвращается системной функцией
QueryPerformanceFrequency()
).Получение системного времени в распространённых операционных системах возможно в двух вариантах: менее точное время [
clock_gettime(CLOCK_REALTIME_COARSE)
в POSIX иGetSystemTimeAsFileTime()
в Windows] и более точное ценой больших вычислительных затрат на его получение [clock_gettime(CLOCK_REALTIME)
в POSIX иGetSystemTimePreciseAsFileTime()
в Windows]. Поэтому функций получения системного времени должно быть две: быстрое менее точное и более точное. [Либо одна функция, но с дополнительным аргументом.]В текущей же спецификации std::chrono получилась такая ситуация, что есть ненужный функционал, а функционала нужного нету. И если вернуться к вопросу голосования «Как с помощью std::chrono получить наиболее точное текущее значение системного времени?», то вообще говоря правильный ответ будет — никак. :)(: Т.к. системное время в std::chrono представляет единственный класс
std::chrono::system_clock
. И в std::chrono нельзя получить менее точное системное время и более точное, а можно получить только просто какое-то системное время. А какое именно — определяет реализация (в той же Windows это может бытьGetSystemTimePreciseAsFileTime()
, а может бытьGetSystemTimeAsFileTime()
).Это ещё почему?
Хотите, всю таблицу вообще в одну строку, а хотите, в виде списка вот так:
Я уж не говорю про возможность куда угодно вставлять [[[комментарии]]]. (Может быть полезно, чтобы давать подписи строкам или ячейкам таблицы.)
Раз уж тут начали рекламировать свои языки разметки, позвольте и мне вставить 5 коп. Вот так данная «таблица в таблице» выглядит в пк-разметке:
Да, набирать такое не очень удобно, но читаемость вполне себе неплоха.
Послушайте. Текущее решение — это не первое, что пришло мне в голову, и что я сразу же бросился реализовывать. И это не первая реализация типа
File
в 11l. Это результат размышлений на протяжении нескольких лет.И принимая данное решение я прежде всего исходил из соображений практичности. С задачами типа «его хэндл/дескриптор можно передать в другой процесс» типичный программист прикладного ПО за всю свою карьеру сталкивается примерно ни разу. Признайтесь честно, ведь и вам не приходилось сталкиваться с такой задачей на практике?
К тому же, тип
File
в 11l — это аналогstd::fstream
(а точнее,std::ifstream
/std::ofstream
) в C++. Вас же не смущает невозможность передатьstd::fstream
в другой процесс?Для обёртки вокруг низкоуровневого файлового дескриптора будет использоваться отдельный тип (вроде
FileDescriptor
/FileHandle
или дажеos:File
), когда он понадобится. А короткое имяFile
гораздо больше смысла имеет оставить за реализацией, которая и требуется чаще всего при типичной работе с файлами (прочитать или записать что-то в файл).Если файл читается/пишется не целиком, то «всего-то позвать функцию ОС» не достаточно, т.к. вызов функции ОС осуществляет переключение в Ring 0 и обратно, что очень дорого. Для эффективной работы без буферизации чтения/записи не обойтись.
Если такое и потребуется, то лучше использовать отдельный тип для этого (
IOFile
например). ТипFile
в 11l ориентирован на типичную работу с файлами, причём эффективную (за счёт буферизации) и безопасную (за счёт проверок на этапе компиляции).Просто я фанат предельной краткости, но в данном случае, видимо, это обернулось против меня.
Да, наверное, так и следует сделать, но я пока что не определился с названием этого namespace (всё-таки ffh — это неформальное название библиотеки, и мне не очень хочется его фиксировать в исходном коде).
Это проблема языка программирования, которую совсем не обязательно перекладывать на программиста (жертвовать красотой API в угоду производительности). К примеру, 11l прекрасно оптимизирует запись
line = f.read_line()
в аналогstd::getline(f, line);
. [Транспайлер 11l → C++ уже поддерживает эту оптимизацию, но только для записи в форме<str_var> = <file_obj>.read_line()
, а записьvar <new_str_var> = <file_obj>.read_line()
внутри цикла будет оптимизироваться в 11lc.]И, к слову, проблема с именем типов для файловых объектов в языке 11l решилась очень оригинальным образом: в исходном коде тип всегда называется просто
File
[также как в языках Ruby, Java/Kotlin, D], но фактический тип определяется на основе режима открытия файла: если режим не указан, то файл открывается для чтения и имеет типFileForReading
, а если указан режимWRITE
илиAPPEND
, то файл имеет типFileForWriting
. Соответственно, набор доступных методов в фактическом типе отличается и вызватьwrite()
у открытого для чтения файла будет ошибкой компиляции. А еслиFile
используется в качестве имени типа аргумента функции, то фактически используется объединение типовFileForReading|FileForWriting|FileForReadingAndWriting
. Причём, фактически используемые имена типов файловых объектов (например,FileForWriting
) в исходном коде на 11l на данный момент вообще недоступны, а доступен только псевдотипFile
.P.S.
Кстати говоря, насчёт оптимизации записи
line = f.read_line()
вf.read_line(line)
. Чисто теоретически, насколько я понимаю, такую оптимизацию можно было бы протащить и в C++! Дело в том, что в C++ есть такая штука, как "as-if rule". Это правило, которое разрешает любые преобразования кода, которые не изменяют наблюдаемое поведение программы. As-if rule, к примеру, разрешает C++ компилятору встраивать любые функции, даже те, которые не имеют пометкиinline
. А здесь говорится о возможности замены типа аргумента функцииconst T&
наconst T
компилятором в том случае, когдаT
является простым типом, напримерint
.Однако, в результате замены
line = f.read_line()
наf.read_line(line)
паттерн работы со строкойline
будет отличаться [отличие заключается в "разогреве" строки]. Поэтому, если печатать в цикле значениеline.capacity()
, то компилятор не сможет применить "as-if rule", т.к. оптимизация в данном случае изменит наблюдаемое поведение программы. Однако, если обращений кline.capacity()
в коде нет, и если глобальный оператор new не переопределён [в случае данной оптимизации оператор new будет вызываться гораздо реже], то наблюдаемое поведение программы не поменяется и C++ компилятор вполне может применить такую оптимизацию!Такое тоже будет работать, но что это даёт? Просто будут выполняться некоторые лишние действия (
fh.close();
вызовется повторно в деструктореfh
, аbuffer_pos = 0;
хотя ничего и не испортит, но избыточно).Чтобы делать флуширование, необходим доступ к
buffer
иbuffer_pos
объектаOFile
. Т.е. в вашhandle_holder
помимо самого handle, необходимо добавить указатель на объектOFile
,std::function
или что-то вроде того.Вы так говорите, как будто это что-то плохое. :)(:
А вот что действительно печально, так это то, что конструктивной дискуссии у нас совсем не получается.
Я предлагаю [не только вам, а вообще всем читателям] простое решение вполне конкретной проблемы. Вы же всеми силами пытаетесь придумать, почему такое решение неправильно, и даже не хотите признавать наличие проблемы как таковой (проблемы очистки ресурсов в move assignment operator).
А если использовать
move_assign()
, то ничего модифицировать не придётся. А функцияswap()
будет генерироваться автоматически на основе move constructor и move assignment operator. Да, получится чуток менее эффективно ручной реализацииswap()
, но... если честно, для чего может потребоватьсяswap()
у объектов вродеOFile
для меня остаётся загадкой.Для заменяемых типов (transparently replaceable) — не нужен. (Т.к. в данном случае имеет место реконструкция объекта того же самого типа, т.е. тип исходного объекта и вновь созданного полностью совпадают.) Но давайте не будем спорить на тему того, в чём мы детально не разбираемся. :)(:
Или у вас есть конкретный пример кода использования
move_assign()
, который некорректно работает без этогоstd::launder
?А куда его девать, это возвращённое значение? Мне вообще-то в данном случае нужен просто вызов move constructor-а. И мой код основан на реализации
std::allocator<T>::construct()
. Почему явный вызов конструктора в C++ записывается через placement new — вопрос не ко мне. Если можно вызвать move constructor как-то более правильно — дайте знать как.Проблема. Т.к. одного умения делать close, увы, не всегда достаточно. Конкретный пример: деструктор в классе OFile вызывает метод
flush()
, который записывает все оставшиеся данные в буфере в файл. [Больше ничего деструктор классаOFile
не делает, т.к. handle закрывается в деструкторе классаdetail::FileHandle
(сразу после выполнения кода деструктораOFile
).]Если взять вашу реализацию
handle_holder
, то толку от наличияTRAITS::destroy()
будет немного, т.к. перед закрытием handle необходимо вызватьflush()
у объекта-владельца handle, и в итоге move assignment operator для классаOFile
реализовывать придётся как-то так:С помощью функции
move_assign()
можно, во-первых, существенно сократить код реализацииoperator=(OFile &&)
[вот эта реализация] и, во-вторых, что более важно: при добавлении новых переменных-класса вOFile
обновлять код реализацииoperator=(OFile &&)
не потребуется.Так что мой вопрос остаётся в силе: было бы интересно увидеть, какую альтернативу моему решению предлагаете вы?
[Уж случаем не идиому ли "make temporary then swap"? :)(:]
Так ведь давно уже все договорились.
Ответы: 0, 1, 1, 1, 2.
Код чтения файла по строкам для языков Python, C++ и Rust я уже приводил в этом комментарии.
Только что проверил в Nim [взяв за основу этот код, а также используя lines] и в Go [на основе кода отсюда]. Результат такой же.
Отличие в разных языках только в том, включается ли в прочитанную строку символ
\n
или нет (и функцияread_line()
в ffh имеет опциональный аргументkeep_newline
для выбора желаемого поведения).Изначальный посыл статьи был в неочевидной работе метода
eof()
в C++. И неправильное количество прочитанных строк получалось именно из-за него.Насколько я понимаю, для обычных файлов такого произойти не может. Это только для "slow" devices (terminal, pipe, socket).
И ни в одной реализации стандартной библиотеки языка Си (для Linux и FreeBSD) я не увидел проверки на EINTR в коде fread/refill: везде в случае, когда read возвращает число < 0, сразу возвращается ошибка. Поправьте, если ошибаюсь.
Возврат 0 — это ведь всегда признак конца файла, а не "прочитано 0 байт". (Т.е. функция read никак не может проинформировать о том, что было успешно прочитано 0 байт.)
Когда читаете файл последовательно сразу из нескольких параллельных потоков?
Как я уже говорил выше, это ультра редкий кейс, для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%). [И если такое и правда нужно — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.]
Вы так говорите, как будто их кто-то использует. :)(:
Я вот узнал об этих функциях только в процессе написания данной статьи. Программисты хотят решать прикладные задачи, а не погружаться в детали реализации каждой библиотечной функции, которую они используют в своём коде.
К тому же, в файловых потоках C++ unlocked версий нет.
Чтобы делать «просто
*dest = std::move(other)
» кто-то этот оператор присваивания перемещением должен реализовать: либо программист, либо компилятор. И функцияmove_assign()
нужна для упрощения реализации этого оператора.Понять, для чего нужен
move_assign()
, можно только на практике. Если у вас есть реализованный move assignment operator, то я могу показать, как его код можно упростить с помощью функцииmove_assign()
.Но, за неимением вашего кода, давайте покажу на примере кода от Microsoft.
Все эти строки можно заменить на одну строку
move_assign(this, std::move(other));
.Ну я же привёл ссылку на коммит.
Благодаря UniqueHandle и move_assign код класса FileHandle сократился на 10 строчек.
Но, что даже более важно, благодаря UniqueHandle можно добавлять в FileHandle новые переменные-класса только в одном месте — в самом классе, т.е. править код move assignment operator и move constructor при этом не нужно, т.к. новые переменные-класса будут учитываться автоматически в сгенерированном компилятором коде move assignment operator и move constructor.
Если вы считаете, что UniqueHandle не нужен, то было бы интересно увидеть, какую альтернативу моему решению предлагаете вы (желательно не просто на словах, а в виде конкретного кода).
Весь proposal, по сути, это одно предложение из последнего абзаца, смысл которого заключается в том, чтобы C++ компилятор автоматически генерировал реализацию move assignment operator на основе деструктора и пользовательского move constructor. Это предложение не вводит никаких новых понятий (ни move_assign, ни UniqueHandle), а позволяет просто писать меньше кода, вот и всё.
Ответ в виде мини-статьи
О,
move_assign()
, а также классUniqueHandle
— это моя новаторская технология. :)(:Недавно я добавлял переменную-класса, у которого был move-конструктор. В класс-то я добавил, а вот move-конструктор поправить я забыл. Когда обнаружил ошибку, я задался вопросом: как бы сделать так, чтобы компилятор автоматически обновлял/корректировал move-конструктор при добавлении переменных-класса? Тут я вспомнил, что если явно move-конструктор не объявлять [либо объявлять без тела с пометкой
= default;
], то он генерируется компилятором автоматически. Хорошо, а что мешало в данном случае генерировать компилятору корректный move-конструктор [или, другими словами, почему не подходил сгенерированный компилятором move-конструктор, почему он некорректный]? Проблема оказалась в одной единственной переменной-класса типаHANDLE
, которая в move-конструкторе инициализировалась значением из другого объекта, а в этом другом объекте устанавливалась вINVALID_HANDLE_VALUE
(т.е. что-то вродеhandle = other.handle; other.handle = INVALID_HANDLE_VALUE;
).«А что, если этот handle обернуть в некий вспомогательный класс?» — подумал я.
Так родился UniqueHandle.
("Unique", т.к. он запрещает копирование, а позволяет только перемещать себя, по аналогии с
std::unique_ptr
.)UniqueHandle<HandleType, HandleType default_value>
пытается вести себя как handle типаHandleType
: у него определены оператор неявного приведения к типуHandleType
, а также оператор присваивания значения типаHandleType
. Но в отличие от сырого handle, переменная-класса типаUniqueHandle
умеет себя перемещать (кодомhandle = other.handle; other.handle = default_value;
) и автоматически инициализироваться значениемdefault_value
.Так я избавился от объявления move-конструктора.
И т.к. это происходило во время разработки библиотеки ffh, я решил, что переменную
handle
/fd
в классеFileHandle
можно также заменить наUniqueHandle
. Вот соответствующий коммит.Далее встал вопрос — а что делать с оператором присваивания перемещением (move assignment operator)? Если посмотреть на код уже существующих операторов присваивания перемещением, то легко заметить, что сначала идёт освобождение ресурсов, а затем перемещение, причём код освобождения ресурсов в точности совпадает с кодом деструктора, а код перемещения совпадает с кодом move-конструктора.
Таким образом, реализацию оператора присваивания перемещением при наличии move-конструктора можно генерировать автоматически! Именно это и делает вспомогательная функция
move_assign()
: сначала вызывает деструктор (dest->~Ty();
), а затем — move-конструктор (new(dest)Ty(std::move(other));
).К слову, Microsoft предлагает делать наоборот: «you can eliminate redundant code by writing the move constructor to call the move assignment operator». (Т.е. предлагает реализовывать move constructor как
*this = std::move(other);
.) Но такое решение хуже тем, что реализация move assignment operator сложнее реализации move constructor, а также тем, что при этом выполняются лишние действия: вначале конструируется пустой объект, который сразу же будет разрушен внутри реализации move assignment operator (т.е. выполняется цепочка «конструктор-по-умолчанию—деструктор—move-конструктор»). Поэтому лучше выражать move assignment operator через move constructor, а не наоборот.Почему бы не реализовать в классе
UniqueHandle
ещё и move assignment operator? Потому, чтоUniqueHandle
не умеет корректно закрывать handle. И даже если его научить (посредством дополнительного шаблонного параметра — класса, у которого определена статическая функцияclose()
, которая закрывает handle), то всё равно иногда требуется произвести какие-то дополнительные действия перед тем, как закрывать handle, например flush — записать оставшиеся в буфере данные в файл, handle которого находится в данномUniqueHandle
. (Т.е. иногда нужны такие дополнительные действия перед закрытием handle, которые никак не получится сделать на уровнеUniqueHandle
.)На данный момент C++ компилятор автоматически добавляет удалённый [т.е. помеченный как
= delete;
] оператор присваивания в случае, когда в классе есть определяемый пользователем конструктор перемещения (а определяемого пользователем оператора присваивания нет). Но я считаю, что компилятор C++ в этом случае может автоматически генерировать реализацию move assignment operator на основе деструктора и пользовательского конструктора перемещения, таким образом делая в точности то, что делает функцияmove_assign()
. Возможно, если не поленюсь, напишу proposal к стандарту C++. :)(:Если функцию
move_assign()
"реализовать" как*dest = std::move(other);
, то при выполнении кодаdetail::FileHandle<true> fh; fh = detail::FileHandle<true>();
получится бесконечная рекурсия и, как следствие, переполнение стека. Т.к.FileHandle::operator=(FileHandle &&fh)
реализован какmove_assign(this, std::move(fh));
.Это жёсткий диск 2009 года на 250 Гб.
Да, его, конечно, можно отнести к «множеству существующих в настоящее время HDD», но в статье речь была об «используемых в настоящее время». Вы правда используете его по назначению в 2024 году?
Хотелось бы уточнить этот момент. Можете сообщить название/модели ваших HDD?
Очень сомневаюсь, что там 512n (n — native), а не 512e (e — emulation).
Это ультра редкий кейс [если мы говорим про последовательное буферизованное чтение файла, а не выборочное чтение блоками из разных мест файла по заданному смещению], для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%).
Обычно всё-таки каждый поток использует свой отдельный дескриптор файла, а если прям нужно параллельное чтение из одного дескриптора — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.
В современных компиляторах исключения создают совсем небольшой оверхед (в том случае, когда они срабатывают только в исключительных случаях, т.е. обычно не срабатывают вообще).
Я провёл тесты с опциями компилятора /EHs-c- /D_HAS_EXCEPTIONS=0, дополнительно заменив в исходном коде ffh все
throw
наabort()
, и не обнаружил разницы в производительности чтения посредством библиотеки ffh. Однако чтение посредствомstd::ifstream
благодаря отключению исключений ускорилось процентов на 20-30. (Хотя, похоже, это связано не с самими исключениями, а с изменением логики работы при отключении исключений.)Да, в отличие от простого
sys.stdout.write()
у функцииprint()
есть дополнительный аргументend
, который по умолчанию равен символу новой строки\n
.Упрощённо, реализацию
print()
с одним аргументом в Python можно записать так:Более подробно, как работает
print()
в реализации Python можно посмотреть здесь.Так происходит только в Windows. Причём на уровне записи в текстовый файл.
Хотя фактически текстовые файлы с разделителями строк в стиле Unix (одиночный
\n
, код символа — 10) уже давно поддерживаются всеми программами под Windows. Даже встроенный в Windows 10 блокнот научился нормально открывать такие файлы.И то, что при сохранении текстовых файлов Блокнот или Microsoft Word разделяют строки парой символов
\r\n
, это исключительно по религиозным соображениям — в Microsoft не хотят признавать, что это не имеет смысла.Ваш код будет выводить лишнюю пустую строку в конце. Если файл
f.txt
состоит всего из одной строки (причём даже не важно, есть ли символ новой строки в конце файла или нет), то ваш код вызовет функциюprint()
два раза, хотя строка в файле одна.Очень похоже, что вы взяли этот Python-код из какой-то левой статьи, вроде такой. В мануале такого бреда не встретишь.
Если и читать файл посредством
readline()
, то код обычно используют такой (на основе примера отсюда):Эти устройства лишь сообщают о том, что размер сектора у них 512 байт. Но физически размер сектора уже везде 4096 байт и более. Вы разве не слышали про 512e?
Именно. Т.е. когда вы запрашиваете у устройства 512 байт, то фактически будет читаться 4096 байт и более. Поэтому запрашивать меньше 4096 байт не имеет смысла.
Укажите номера моделей ваших HDD. Очень сомневаюсь, что там 512n.
К примеру, в моём ноутбуке 2014 года был установлен HDD ST500LT012-1DG142 на 500 Гб, который я уже давно заменил на SSD. Так вот, в спецификации от Seagate написано:
Bytes per sector 512 (logical) / 4096 (physical)
Неужели ваши диски более старые?
Вы имеете в виду
getline()
?А на что вы предлагаете заменить
getline()
?Т.к. строки в файле идут в виде
<слово> - <перевод>
, то при чтении строкиparser - синтаксический анализатор
посредством кодаf >> s
прочтётся 4 строки:parser
,-
,синтаксический
ианализатор
.Вот соответствующий код на C++
(Ссылка на playground)