Как стать автором
Обновить
51
0

Пользователь

Отправить сообщение
  1. Тем, что в URL можно указать параметры печати pdf-файла: dpi (разрешение), двухсторонняя печать и прочее.

  2. Можно отследить прогресс печати (очень полезно, если 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) и дали функциям получения времени такие мудрёные названия, которые невозможно запомнить.

А можно было поступить так:

  1. Не мудрить с std::ratio и функцией получения текущего системного времени возвращать всегда количество наносекунд с начала эпохи. Т.к. POSIX-функция clock_gettime() возвращает время в наносекундах, а наиболее точное системное время в Windows возвращается функцией GetSystemTimePreciseAsFileTime() в 100-наносекундных интервалах, то получается, что одна наносекунда является общим знаменателем.

  2. Предоставить функцию, возвращающую точность измерения времени (в POSIX точность можно получить через вызов clock_getres(), в Windows возвращать константу 100).

  3. Частоту таймера высокого разрешения сделать зависящей от реализации константой времени выполнения (в std::chrono частота/период таймера является константой времени компиляции, что плохо согласуется с реализацией таймера высокого разрешения в Windows, где частота возвращается системной функцией QueryPerformanceFrequency()).

  4. Получение системного времени в распространённых операционных системах возможно в двух вариантах: менее точное время [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()).

А в pq, мы всё также вынуждены все ячейки одной строки, разместить в одной исходной строке.

Это ещё почему?

Хотите, всю таблицу вообще в одну строку, а хотите, в виде списка вот так:

T‘
H‘‘Col 1’
  ‘Col 2’’

 ‘‘Cell 1.1’
  ‘Cell 1.2’’

 ‘‘Cell 2.1’
  ‘Cell 2.2
T‘H‘‘Col1’ ‘Col2’’
   ‘‘C11’  ‘C12’’’’’
’

Я уж не говорю про возможность куда угодно вставлять [[[комментарии]]]. (Может быть полезно, чтобы давать подписи строкам или ячейкам таблицы.)

Раз уж тут начали рекламировать свои языки разметки, позвольте и мне вставить 5 коп. Вот так данная «таблица в таблице» выглядит в пк-разметке:

T‘
H‘‘Col 1’     ‘Col 2’’
 ‘‘Cell 1.1’  ‘Cell 1.2’’
 ‘‘Cell 2.1’  ‘Cell 2.2
[[[    ]]]T‘H‘‘Col1’ ‘Col2’’
             ‘‘C11’  ‘C12’’’’’
’

Да, набирать такое не очень удобно, но читаемость вполне себе неплоха.

Отличный пример отвратительной реализации чего-то что этим не является.

Послушайте. Текущее решение — это не первое, что пришло мне в голову, и что я сразу же бросился реализовывать. И это не первая реализация типа 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 обернуть в некий вспомогательный класс?» — подумал я.

Так родился 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++. :)(:

Почему нельзя было просто сделать *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));.

Это жёсткий диск 2009 года на 250 Гб.

Да, его, конечно, можно отнести к «множеству существующих в настоящее время HDD», но в статье речь была об «используемых в настоящее время». Вы правда используете его по назначению в 2024 году?

множество существующих в настоящее время HDD всё ещё имеют размер сектора 512 байт. У меня таких есть несколько

Хотелось бы уточнить этот момент. Можете сообщить название/модели ваших HDD?
Очень сомневаюсь, что там 512n (n — native), а не 512e (e — emulation).

несколько потоков читают из-одного файлового дескриптора (странный кейс, но в целом возможный и валидный)

Это ультра редкий кейс [если мы говорим про последовательное буферизованное чтение файла, а не выборочное чтение блоками из разных мест файла по заданному смещению], для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >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, -, синтаксический и анализатор.

Вот соответствующий код на C++

(Ссылка на playground)

#include <string>
#include <fstream>
#include <iostream>

using namespace std;

int main() {
    ifstream f("/uploads/words.txt");
    string s;
    int total = 0;

    while (f >> s) {
        cout << s << endl;
        total++;
    }
    
    cout << "Total lines: " << total << endl;
}

Информация

В рейтинге
Не участвует
Откуда
Владивосток, Приморский край, Россия
Зарегистрирован
Активность