Pull to refresh

Comments 134

интересно, почему в подобных статьях, написанная самостоятельно либа ВСЕГДА быстрее std))

Извиняюсь если неправильно понял проблему, статью не очень внимательно читал.

Дело в том, что читается дополнительно пустая строка, если в конце файла есть \n?

Я проблемы в этом не вижу. Собственно что такое строка? Как я понимаю - всё, что стоит между началом/концом/'\n' и \n. Сам символ называется newline. Это значит, что строк столько же, сколько и этих символов + 1 вначале файла.

интересно, почему в подобных статьях, написанная самостоятельно либа ВСЕГДА быстрее std))

Много фич, не все из которых на 100% zero-cost. Стандарт иногда накладывает требования, которые нельзя удовлетворить не заплатив производительностью( например pointer stability у unordered_map )

я к тому и веду, что это вообще не равносильное сравнение. И тут уже проблема либо в том, что авторы либ делают крайне малую часть функционала, либо проблема к стандарту, который ставит такие требования, либо к разработчикам компиляторов

Ну да, как тут правильно заметили подход автора ломает поддержку multi threading. Так что да, если отказаться от части фич, то можно сделать быстрее. Если забить на переносимость, например, то можно вообще поюзать mmap/MapViewOfFile и получить еще нефиговый прирост в скорости.

Тоже не понял, с чем воюет автор.
Добавил символ переноса строки == добавил в файл пустую строку

Не нравится пустая строка - ну не сохраняй ее себе…

Не нравится пустая строка - ну не сохраняй ее себе…

А как вы предлагаете отличать пустую строку в самом конце файла от пустых строк в середине?

Хорошо, если все пустые строки можно просто игнорировать. Но в некоторых задачах промежуточные пустые строки имеют значение.

сделайте trim, чтобы не заморачиваться

А как вы предлагаете отличать пустую строку в самом конце файла от пустых строк в середине?

Вы ведь сами ответили на свой вопрос - если текущая строка пуста и настал конец файла.

Конец файла будет выставлен при следующем чтении

Нет, при текущем.

Сначала дочитает до последнего \n и вернёт эту строку, затем начнёт читать следующую, и получит EOF. Автор спросил, как вот эту последнюю строку отличить от всех остальных - ответ - при ее чтении прочтется 0 байт и будет EOF.

В некоторых случаях 0 байт будет даже и не при пустой строке. В других случаях будет не нол байт и не EOF, а ошибка при чтении следующей строки. Это не переносимый случай. Очень частая ошибка с различными вебсерверами и приложенияи на "экзотике". гарантирован только случай когда новая строка начинается с EOF

А игнода конец файла встаёт сражу же, поэтому если у нас последняя строка не пустая ее можно проигнорировать на одной ОС... А на другой флаг не выставиться, на третей последняя строка не считается

Тоже не понял, с чем воюет автор.

видимо воюет со студентами:

Так вот, у студентов, выбравших для выполнения этого задания язык C++ ... почему-то иногда читалась из файла лишняя пустая строка.

строка может быть иногда лишней только когда вы совсем НЕ понимаете зачем вы файл читаете. Видимо студенты достали преподователя требуя объяснить смысл этого чтения, но он, вместо этого, видимо в отместку, еще сильнее их хочет запутать, ведь это он преподаватель, а они студенты - бестолковые по определению. Студенты должны благоговейно относиться к преподавателю и не задавать умных вопросов для которых преподавателю приходится читать документацию.

Зря вы так.

На уточняющие вопросы студентов я всегда охотно отвечаю, но прямого вопроса «почему у меня читается лишняя пустая строка» я не получал, а заметил это сам в процессе отладки решения одного из студентов.

«Смысл этого чтения» излагается в тексте задания и в данной статье не приводится, так как приводить полный текст задания в данной статье я посчитал излишним. Суть в том, что на входе есть текстовый файл со списком слов на английском языке и их переводом.

Каждая строка файла содержит:

<слово> - <перевод>

Конкретно в данной задаче пустые строки можно просто игнорировать, или можно удалять последнюю строку из массива прочитанных строк (в этом случае программа станет работать некорректно после добавления во входной файл строки, которая не оканчивается на символ новой строки), что, собственно, и делали студенты, пока я не обратил на это внимание и не решился разобраться подробно.

Формально файл в котором есть строка не оканчивающаяся концом строки (а например символом конца передачи) является некорректным текстовым файлом. Символ конца файла обязан следовать за концом строки. Но это такая частая ситуация, что от нее страхуются

Даже если строка закончится концом файла без LF и CR, она все равно должна быть считана и обработана как строка. LF и CR работают как разделители строк, а последнюю строку отделять не от чего.

написанная самостоятельно либа ВСЕГДА быстрее std))

ктото забыл собрать/тестировать Release))

std в Debug очень не быстрая

да и в релизе не сильно быстрее, увы

Я проблемы в этом не вижу.

Смотрите, в чём проблема. Допустим, есть такая программка на Python:

print('a')
print('b')

Она выводит в консоль две строки:

a
b

Теперь, если перенаправить вывод этой программы в файл (python prog.py > out.txt), то логично будет ожидать, что в файле будут всего две строки, а не три ['a', 'b' и ''].

Это значит, что строк столько же, сколько и этих символов + 1 вначале файла.

Если следовать такой логике, то получится, что в файле out.txt три строки (два символа \n + 1), а должно быть две.

Любая внятная реализация чтения строк, воспринимает EOF как конец строки. Что охренеть логично, ведь если у вас файл

aaaa
bbb
cc

То у вас 3 строки. И если у вас файл

aaaa
bbb

У вас тоже 3 строки! Одна просто пустая. Что просто напросто логично)

*форматирование хабра съело пустую строку в во втором примере, но вообразите, что она есть.

Для текстовых файлов перевод строки в саму строку не входит.

\n же не просто буква, это newline. Раз строка, новая - два строка, новая - три строка.

А ведь и действительно три. Согласен. Теперь я адепт секты трех строчек :)

Теперь я адепт секты трех строчек :)

Теперь напечатайте эти 3 строчки 3 print'ами. Количество прочитанных строк должно быть равно количеству напечатанных, правда?

Нормальный print не должен проявлять самодеятельность с началом новой строки, когда ему вздумается, причём скрыто от пользователя.

Ну, тогда нужно разобраться, как же так - напечатали 2 строки а прочитали 3. В одном из алгоритмов ошипко ;)

Если print не добавляет начало новой строки скрытно, всё становится гораздо понятнее:

<< STR1

<< endl

<< STR2

В файле получаем две строки, одно начало новой строки. Никаких проблем.

Проблема возникает, когда логика print неявно добавляет endl после каждой строки:

<< STR1 << endl

<< STR2 << endl

Она сама начала третью строку. Пользователь хотел вывести две, а реализация вывела в файл три. Это можно рассматривать как ошибку или как особенность реализации.

Пользователь хотел вывести две, а реализация вывела в файл три.

Напечатайте же эти 3 строки ;)

Или даже так: допустим, все строки пустые. Пользователь печатает 2 пустых строки, программа читает 3 пустых строки. Что-то не так, не правда ли?

Три строки в кавычках: "STR1", "STR2", ""

Три пустые строки в файле - это два байта \n\n.

Я уже объяснил, что не так: непрозрачная реализация print. Честно говоря, я сильно удивлён, как много людей в комментариях не понимает такую простую вещь. Наверное, они не печатали на печатной машинке и не нажимали клавишу перевода строки.

python -c "print('a'); print('b')" | python -c "import sys; print(sys.stdin.readlines())"
['a\n', 'b\n']

2 строки напечатано - 2 прочитано. Все прозрачно

Ну, допустим, мы любители вставлять переводы строчек вручную, потомушто стандартный print сложное.

python -c "import sys; sys.stdout.write('a\nb')" | python -c "import sys; print(sys.stdin.readlines())"
['a\n', 'b']

2 строки напечатано - 2 прочитано.

Напишите ваш код, который из 2 строк делает 3.

У вас всюду напечатано три строчки. \n - это newline, переход на следующую строку. На печатной машинке или телетайпе у вас физически перед глазами после этих действий третья строка. Как, в общем-то, в любом текстовом редакторе: откройте пустой файл, нажмите Enter два раза, посчитайте строки.

В Питоне readline как и print скрывает пустую строку в конце файла. Как раз чтобы не возникало лишних вопросов, и казалось, что всё прозрачно.

В Питоне readline как и print скрывает пустую строку в конце файла

Напишите код, который делает из 2 строк 3, на любом другом языке ;)

Ну, допустим, мы любители вставлять переводы строчек вручную, потомушто стандартный print сложное.

Итак, при чтении 'a\nb', использованные вами функции Python'а считают, что в первой строке -- символ a, а во второй -- символ b, потому что именно так 'a\nb' было разбито на строки:

$ python -c "import sys; sys.stdout.write('a\nb')" | python -c "import sys; print(sys.stdin.readlines())"
['a\n', 'b']
$ 

Теперь, если отредактировать содержимое второй строки, убрав символ b, тем самым, делая вторую строку пустой, то получится 'a\n'.

Поскольку символы '\n' при этом никак не были затронуты, количество строк не должно измениться, ведь было всего лишь отредактировано содержимое второй строки, верно?

Но теперь те же самые функции Python'а отказываются видеть вторую строку:

$ python -c "import sys; sys.stdout.write('a\n')" | python -c "import sys; print(sys.stdin.readlines())"
['a\n']
$ 

При этом, если сделать наоборот, отредактировав точно так же первую строку до пустой ('\nb'), функции Python'а продолжают видеть две строки:

$ python -c "import sys; sys.stdout.write('\nb')" | python -c "import sys; print(sys.stdin.readlines())"
['\n', 'b']
$ 

Интересная картина.

Нет, нету тут нигде ошибки. Эти print/input сделаны для работы с консолью. Тут всегда надо выводить перевод строки, а eof исключителен - если программе надо что-то прочитать, она обычно ждет, пока пользователь, таки, соизволит напечатать и нажать enter. Пользователь, конечно, может и ctrl-z (или что там) нажать, но это обычно большая редкость.

Для работы с файлами есть file.write - и оно переводы строки само не добавляет.

Если же говорить о каком-нибудь C, на которых пишут программы, которые в консоли через pipeline цепляют друг за другом, то там printf не вставляет переводы строк само незаметно для программиста. И описанного вами несоответствия количества строк обычно нет.

Если тут и есть ошибка, то в дизайне функций в библиотеке питон, делающим их применение в файловом вводе-выводе нелогичным.

Ок.

python -c "with open('text.txt', 'w') as f: f.writelines(['one', 'two'])"
hexdump -C text.txt 
00000000  6f 6e 65 74 77 6f                                 |onetwo|

Выводим в файл 2 строки - записалась 0 разделителей. Документация советует за разделителями строк следить лично. А то получится так:

python -c "with open('text.txt', 'w') as f: f.writelines(['', '', ''])"

Сколько строк в файле? ;)

Документация советует за разделителями строк следить лично

Ну так это проблема библиотеки питон, а не "что считается строчкой".

Ну так это проблема библиотеки питон, а не "что считается строчкой".

fprintf(f, "%s","");
fprintf(f, "%s","");
fprintf(f, "%s","");

Сколько строк в файле? ;)

Проблема как раз в том, что считать строкой во время чтения. А то я записал 3 строки, но прочитать их не получится.

Одна. Пустая. Что не так?

Что не так?

Записано 3 пустых. 3!=1

Я к чему это все. Процедуры записи и чтения должны быть полностью симметричными. Что записано - то и прочитано. Записано N строк - должно быть прочитано тоже N, не N+1. Если у кого-то получается иначе - ну, значит он что-то сломал.

Нет. Вы 3 раза "не вывели ничего". Можете хоть 33 раза это сделать - результирующий файл будет идентичен.

Если вы хотите вывести пустые строки - то вам надо их разделять '\n'.

Если вы хотите вывести пустые строки - то вам надо их разделять '\n'.

Ок. В файле ровно 1 символ, и тот \n
Сколько строк записано?

Две - обе пустые.
Вот вам другой пример. Вот сколько в этой строке (из csv) записей, разделенных запятой: "first,second,third"? Три же, правда? И даже питон через ','.split получит 3 записи.

В "first,,second" - тоже три записи. Вторая запись пустая, но это нормально. "a," - две записи, "," - тоже две.

Вот в файле аналогично: строки - это записи, разделенные '\n'.

Две - обе пустые.

Не угадали, одна ;)

char empty_string[] = "";
fprintf(f,"%s\n", empty_string);

Или две?

with open("file.txt", "w") as f:
  f.write("\n".join(['', '']))

Зависит от соглашения между читателем и писателем.

вот! Полноценная замена статье получилась всего в 5 (+-) строчках!

Записано 3 пустых. 3!=1

Нет, была "дописана" одна и та же строка.
Причём, в вашем случае -- вы уверены, что вообще реальная запись осуществлялась хоть раз?

Но даже если взять предыдущий случай с "one" и "two", -- опять же, происходила дозапись одной и той же строки.

Чтобы начать новую строку, необходимо записать разделитель newline, и пока этого не сделано, происходит дозапись одной и той же строки.

А если нужно записать одну строку, но 3-мя независимыми вызовами print/write, тогда что делать, если следовать вашей логике и всегда записывать разделитель строк? (собственно print так и поступает по-умолчанию, а write - нет). Все это разнообразие существует только лишь по-тому, что нет единственного правильного решения во всех случаях. В одном случае print должен выводить строку и завершаеть ее \n, а в другом, нужно несколько вызовов print/write для формирования строки, а только последний с \n, например. В библиотеках разных языков подходят по-разному к тому, как должны вести себя функции ввода-выводы. Это конечно путает, но это не значит, что где-то поступают неправильно, а где-то нет, ибо правильно не существует в этой задаче.

Сколько строк в файле? ;)

Если поискать online-сервисы подсчёта строк и выбрать те из них, которые не отказываются считать количество строк в пустом тексте...

Возможно, вы будете смеяться, но ответ будет 1.

Вот один такой сервис, вот -- другой.

Второй сервис позволяет загрузить свой файл.
Вы можете загрузить туда свой text.txt и увидеть результат.

Неправда. print печатает строку с переносом, и именно это он и сделает, напечатав перенос после третьей строки, что создаст четвертую, пустую строку.

Если нужно печатать строки без переноса, можно написать для этого специальный инструмент (не знаю, есть ли готовое решение, редко пишу на python). Который, например, для каждой строки, начиная со второй, будет писать символ переноса в начале, а не в конце строки.

Просто не нужно ожидать от инструмента то, для чего он не предназначен.

у вас здесь три строки, две строки это

a
b[terminal greeting]

Я же говорю - вам нужно определиться с тем, что такое строка

Вы уверены? на линукс две строки в текстовом потоке это

a [\n]

b [\n]

[end-of-transmission]

опять же, что вы понимаете под строками? Для меня их count('\n')+1, а сами строки это / .*/ . По ощущениям, для вас строки это / .+/

Если вы понимаете под строками чисто визуальные строки, то как вы отличаете ваш пример в терминале с двумя строками от его же в файле, но строк уже три. В редакторе курсор будет на последней пустой строке. Вы ее за строку не считаете?

a\n

b\n

итого 3 строки с одной пустой

a\n
b\n

-- 2 строки

a\n
b

-- тоже 2 строки

a\n
b\n
\n

-- 3 строки, с одной пустой

??? Начинается всегда очевидно с новой строки, print вывел 2 раза ещё по +1 строке (\n) итого их 3

В конце файла не \n

python -c "print('a');print('b')"|wc -l
2

количество пробельных символов и количество строк это разные числа

python -c "print('a');print('b')"|wc -l

2

Это, конечно, хорошо, что wc -l выдаёт 2.
Осталось выяснить, что означает опция -l.

man wc:

-l, --lines print the newline counts

Да, количество символов '\n' там, действительно, равно 2.

Ну, и что?
Мы это и так знаем.

Мы это и так знаем.

Там немного выше считают, что это 3 строки.

Там немного выше считают, что это 3 строки.

Но вы же писали не об этом.
Вы писали, что wc -l выводит 2.
Намекая на то, что это и есть количество строк.

Однако, wc -l считает количество символов '\n' и ничего не сообщает о том, сколько это означает строк.

Поэтому результат выполнения wc -l никак не проясняет вопрос количества строк.

А то, что там два символа '\n', мы знаем и без wc -l.

a\n
b\n
\n

-- 3 строки, с одной пустой

это 4 строки - последний перевод строки создает еще одну пустую.
вот это три строки

a\n // печатает a, создает пустую строку
b\n // печатает b в новой строке, создает еще пустую строку
// ничего не печатает


В basic
print "a";
print "b"
напечатает ab и создаст пустую строку


Как вы думаете, как работает print? Почему по вашему, две команды print выводят две строки, а не два символа в одну строку? Потому что print по умолчанию печатает текст + переносит строку.

Т.е. по байтам print("а") - это

97, 13, 10

т.е. при печати одного стмвола, печатается 3 байта. Сам байт, возврат каретки и новая строка.

Если бы этого не было, команда print печатала бы текст в одну сплошную строку.

И текстовый файл с двумя твоими строками будет выглядеть так

97, 13, 10, 98, 13, 10

Как вы думаете, как работает 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 не хотят признавать, что это не имеет смысла.

Ну так \r и не играет тут ключевую роль. Ключевую роль играет \n, который при
print('a')
print('b')

будет дважды.

Просто нужно помнить, что есть такая вещь, как символ новой строки. Что он имеет смысл только в текстовых файлах. И что в Windows он составной.

> написанная самостоятельно либа ВСЕГДА быстрее std

Потому что внутри stdlib трэш, угар и содомия. Более того, виновато даже не количество фич, которые надо поддерживать, потому что корме самоделок с ограниченным скоупом существуют также и крупные библиотеки, от серьёзных разработчиков, которые реализуют много фич, активно используются и тоже заметно быстрее.

Проблема i/o в стдлибе в том, что стандарт требует не только реализации многих фич, но диктует и детали этой самой реализации, которые тяжело сделать быстрыми.

.И разные ОС ведут себя по-разному здесь, Си++ наследует библиотеку времени исполнения и поведение ОС. .

(Если хочется защиты от UB из-за чтения одного файла из разных потоков, то можно сохранять id потока в объекте файла при его открытии и в функциях чтения сравнивать id текущего потока с сохранённым, но блокировка при чтении явно излишня.)

Как-то очень наивно звучит. Многопоток вообще вещь сложная. Вот сравнили, и дальше что? Если поток не тот, то что функция чтения делает?

Ага, вот бы например nginx удивился бы, узнав что не может читать файлы не из того потока, которых их открывал.

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

Но это ладно. Меня больше задевает обработка ошибок исключительно через исключения. При том, что куча крупных проектов и почти весь embedded мир намеренно их выключают (see llvm, chromium, google c++ code convention, etc.)

Справедливости ради, гугл их выключает не потому, что ой плохо сейчас, а потому что ой плохо было тогда, когад гугл начинался, а к моменту когда стало удовлетворительно его кодовая база была уже таких размеров, что переезд посчитали нецелесообразным.

Там есть на самом деле проблемы с безопасностью при развёртке стека... И проблемы вообще

В целом да, но емнип, один из основных аргументов против исключений и rtti — дополнительный оверхед, который они создают. Ну и крайняя неочевидность, в каком месте какие могут вылетать исключения, и где какие исключения нужно обрабатывать в больших кодовых базах вроде хромиума.

Или пролет исключений через границы библиотек, например.

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

Это ультра редкий кейс [если мы говорим про последовательное буферизованное чтение файла, а не выборочное чтение блоками из разных мест файла по заданному смещению], для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%).

Обычно всё-таки каждый поток использует свой отдельный дескриптор файла, а если прям нужно параллельное чтение из одного дескриптора — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.

Меня больше задевает обработка ошибок исключительно через исключения.

В современных компиляторах исключения создают совсем небольшой оверхед (в том случае, когда они срабатывают только в исключительных случаях, т.е. обычно не срабатывают вообще).

Я провёл тесты с опциями компилятора /EHs-c- /D_HAS_EXCEPTIONS=0, дополнительно заменив в исходном коде ffh все throw на abort(), и не обнаружил разницы в производительности чтения посредством библиотеки ffh. Однако чтение посредством std::ifstream благодаря отключению исключений ускорилось процентов на 20-30. (Хотя, похоже, это связано не с самими исключениями, а с изменением логики работы при отключении исключений.)

В многопоточность автор зря полез. Не его это.

Несколько комментариев:

  1. Когда вы читаете из файла, вы передвигаете (модифицируете) текущую позицию в файле, поэтому доступ из разных потоков должен быть синхронизирован.

  2. В POSIX есть unlocked версии некоторых функций работы с файлами, например getc_unlocked, также есть функции ручной блокировки flockfile

  3. В glibc в качестве расширения есть unlocked версии многих других функций

  4. Насчет размера буфера по умолчанию это действительно исторический артефакт, и гораздо более ранний. В glibc размер буфера был зафиксирован на 8 kb в 1989 ещё и больше не менялся из соображений совместимости. В MacOS дефолтный размер буфера вообще 1 kb

Внезапно. Я почемуто думал, что чтение из std::stream всегда однопоточное. А чтение из разных потоков - это ub.
Тоесть, я могу написать так?

std::ifstream f(...);
void thread1() {
  f.read();
}
void thread2() {
  f.read();
}

[iostreams.threadsafety]:

Concurrent access to a stream object ([string.streams], [file.streams]), stream buffer
object ([stream.buffers]), or C Library stream ([c.files]) by multiple threads may result in
a data race ([intro.multithread]) unless otherwise specified ([iostream.objects]).

где [iostream.objects] говорит следующее про cin, cout, cerr, clog:

Concurrent access to a synchronized ([ios.members.static]) standard iostream object's formatted and unformatted input ([istream]) and output ([ostream]) functions or a standard C stream by multiple threads does not result in a data race ([intro.multithread]).

[Note 2: Unsynchronized concurrent use of these objects and streams by multiple threads can result in interleaved characters. — end note]

а [ios.members.static] описывает функцию sync_with_stdio(bool) которая управляет, будут ли сin и ко. синхронизированы или нет.

То есть отвечая на ваш вопрос, конкуррентное чтение из произвольного std::ifstream не разрешено за исключением cin.

Что касается С, то стандарт C11 (N1570) говорит следующее:

7.21.2 Streams

  1. Each stream has an associated lock that is used to prevent data races when multiple threads of execution access a stream, and to restrict the interleaving of stream operations performed by multiple threads. Only one thread may hold this lock at a time. The lock is reentrant: a single thread may hold the lock multiple times at a given time.

  2. All functions that read, write, position, or query the position of a stream lock the stream before accessing it. They release the lock associated with the stream when the access is complete.

В стандарте С99 (N1256) такого не было. (А в POSIX вроде было?)

В .POSIX вроде даже синхронизируются через файловый ввод-вывод (файлы-семафоры), но вроде нужны дополнительные шаги. То же с сокетами

Когда вы читаете из файла, вы передвигаете (модифицируете) текущую позицию в файле, поэтому доступ из разных потоков должен быть синхронизирован.

Когда читаете файл последовательно сразу из нескольких параллельных потоков?

Как я уже говорил выше, это ультра редкий кейс, для поддержки которого неразумно замедлять чтение во всех остальных случаях (которых >99%). [И если такое и правда нужно — лучше использовать отдельную/другую реализацию чтения файлов в этом случае.]

В POSIX есть unlocked версии некоторых функций работы с файлами, например getc_unlocked, также есть функции ручной блокировки flockfile

Вы так говорите, как будто их кто-то использует. :)(:
Я вот узнал об этих функциях только в процессе написания данной статьи. Программисты хотят решать прикладные задачи, а не погружаться в детали реализации каждой библиотечной функции, которую они используют в своём коде.
К тому же, в файловых потоках C++ unlocked версий нет.

Тема с "проверкой на EOF" заезжена уже воль и поперек. В программах на стандартной библиотеке С и С++ не должно быть проверок на конец файла вызовом "функций проверки на eof" вроде feof или соотв. методов С++ потоков. За исключением ряда нишевых случаев такая проверка в коде является антипаттерном, т.е. сразу же является красным флагом ошибки.

Функции проверки состояния EoF в С и С++ предназначены для пост-мортемного анализа причин завершения чтения файла. В большинстве кода, как учебного, так и практического, никому такой пост-мортенмный анализ не нужен, поэтому и проверки состояния EoF в коде не должно быть вообще.

Стандартным корректным и хорошо устоявшимся паттерном чтения данных из файла в стандартной библиотеке С и С++ является паттерн "читаем, пока чтение проходит успешно". Успешность чтения проверяется на основе кодов возврата функций чтения. Это практически ясельное FAQ.

Обратите внимание (что, кстати, не отражено как следует в статье): файлы читают не до EoF, а до тех пор, пока не произойдет что-то исключительное, приводящее к отказу операции чтения. И это может быть далеко не только EoF, а также и физические проблемы ввода-вывода, и ошибки формата. Вот это - правильный паттерн, а не фиксация исключительно на EoF.

А как отличить ошибку форматировнного ввода -он ставит поток в состояние bad() - и завершения из-за встреченого конца файла. Ну а feof действительно не нужен, его аналог есть в std::istream

Во-первых, ошибка форматированного ввода ставит поток в состояние fail, а не в состояние bad.

Во-вторых, не понимаю вопроса. Если вы хотите отличить - отличайте. Кто вам не дает? У вас есть доступ ко всем флагам и состояниям: и eof, и fail, и bad... Проверяйте на здоровье все, что хотите. Речь идет о том, что в корректном коде это делается уже после завершения чтения, а не для контроля самого цикла чтения. Как только вы начинаете это делать для контроля цикла чтения - с 99% вероятностью вы пишете ошибочный код.

В-третьих, все что я сказал выше про EoF, разумеется, касается всех функций проверки EoF. Неважно, feof это или что там есть в std::istream.

И это может быть далеко не только EoF, а также и физические проблемы ввода-вывода, и ошибки формата.

Ну если это физические проблемы ввода-вывода, то это серьёзный повод прекратить дальнейшую работу программы вообще. Поэтому чекать EOF всё равно придётся.

То есть, фактически не предусмотрено стандартной причины для остановки чтения файла в виде "это конечная, выходим"? И все, что может остановить неостановимое чтение, есть стрельба в ногу? Даже если пуля вида "конец файла"? И каждый раз при остановке чтения нужно еще выяснять, была ли это именно нужная пуля, а не, скажем, 70мм фугас? Тем, кто придумал эти самые правильные паттерны, в голове ничего не жало, когда они до этого додумались?

В практическом отношении, наличие символа конца строки в конце файла практически не контролируемо пользователем, набирающим файл в текстовом редакторе, поэтому вряд ли тут может быть важно точное различение.

Скорее, жизнь могут попортить лишние символы \x0A при переносе текстовых файлов между Windows в Linux/macOS.

Проблема в интерпретации символа \n - он не означает "конец строки", он означает "сейчас будет следующая".

Он означает буквально newline, новая строка. Если символ есть - значит уже начата ещё одна новая строка.

Вроде-бы автор имеет такой серьезный тех.бэкграунд, но не понимает, что такое "\n" - и от этого вся проблема и борьба с ветрянными мельницами.

"\n" - это символ не конца строки, а символ разделителя строк - и если это держать в уме, то все работает четко и корректно.

Т.е. кол-во строк в файле = кол-во "\n" в файле + 1.

Нет ни одного "\n" в файле - есть только одна строка, есть - минимум две строки.

И так было всегда. Страшно, что это заблуждение транслируется еще и на студентов.

Путаница от того что кое-где это два символа - конец строки(возраст каретки) и новая строка. Блейм майкрософт, вернее PDP-11

"\n" - это символ не конца строки, а символ разделителя строк - и если это держать в уме, то все работает четко и корректно.
Т.е. кол-во строк в файле = кол-во "\n" в файле + 1.
Нет ни одного "\n" в файле - есть только одна строка, есть - минимум две строки.

А вот разработчики языков Python, C++ и Rust с вами не согласны. :)(:

Возьмём для примера два файла with_ending_newline.txt и without_ending_newline.txt.
Первый содержит "a\nb\n", а второй — "a\nb".
Если читать эти файлы по строкам используя readlines()/getline()/read_line(), то будет прочитано две строки во всех случаях:

Python

(Ссылка на playground)

total = 0

for line in open('/uploads/with_ending_newline.txt').readlines():
    print(line)
    total += 1

print('Total lines: ', total)
C++

(Ссылка на playground)

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

using namespace std;

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

    while (getline(f, s)) {
        cout << s << endl;
        total++;
    }
    
    cout << "Total lines: " << total << endl;
}
Rust с использованием lines()

(Ссылка на playground)

// [https://stackoverflow.com/questions/45882329/read-large-files-line-by-line-in-rust <- google:‘rust read lines from file’]
use std::fs::File;
use std::io::{self, prelude::*, BufReader};

fn main() -> io::Result<()> {
    let file = File::open("/uploads/with_ending_newline.txt")?;
    let reader = BufReader::new(file);
    let mut total = 0;

    for line in reader.lines() {
        println!("{}", line?);
        total += 1;
    }
    
    println!("Total lines: {}", total);

    Ok(())
}
Rust с использованием read_line()

(Ссылка на playground)

// [https://stackoverflow.com/questions/63627687/why-does-rusts-read-line-function-use-a-mutable-reference-instead-of-a-return-v <- https://stackoverflow.com/search?q=rust+read_line]
use std::fs::File;
use std::io::{self, prelude::*, BufReader};

fn main() -> io::Result<()> {
    let file = File::open("/uploads/with_ending_newline.txt")?;
    let mut reader = BufReader::new(file);
    let mut total = 0;
    let mut line = String::new();

    while reader.read_line(&mut line)? != 0 {
        println!("{}", line);
        total += 1;
        line.clear();
    }
    
    println!("Total lines: {}", total);

    Ok(())
}

Обратите внимание, вывод для файлов with_ending_newline.txt и without_ending_newline.txt немного отличается, но в обоих случаях Total lines: 2.

автор ... не понимает, что такое "\n" - и от этого вся проблема и борьба с ветрянными мельницами.

Автор пытается обратить внимание читателей на то, что функции feof()/eof() в C/C++ однозначно можно признать неудачными/[сбивающими с толку], т.к. во всех новых языках программирования ничего подобного этим функциям просто нет — в Python вообще никакого eof-а нет, а в Nim, Rust и Swift eof() работает как в старом добром Паскале (ну только называется немного по другому: endOfFile() в Nim, !has_data_left() в Rust, availableData.count == 0 в Swift).

А мануал почитать на туже readline у Python? Вы реально не понимаете, что происходит?

Если да - мне нечего сказать.

Вот верно написанная фя на Python

l = 0
with open( "f.txt", encoding="utf-8" ) as f:
while True:
line = f.readline()
print ( l, line )
l = l +1
if ( "" == line ): break

Чтение файла заканчивается на пустой строке. Ну, может так автору было надо ;)

Ваш код будет выводить лишнюю пустую строку в конце. Если файл f.txt состоит всего из одной строки (причём даже не важно, есть ли символ новой строки в конце файла или нет), то ваш код вызовет функцию print() два раза, хотя строка в файле одна.

А мануал почитать на туже readline у Python?

Очень похоже, что вы взяли этот Python-код из какой-то левой статьи, вроде такой. В мануале такого бреда не встретишь.

Если и читать файл посредством readline(), то код обычно используют такой (на основе примера отсюда):

with open("myfile.txt") as fp:
    while True:
        line = fp.readline()
        if line == "":
            break
        print(line.rstrip())

\n - это перевод строки и его кодировка зависит от целевой системы. В винде это будет CRLF или 0D0Ah. И видимо из-за всяких упрощений, выкидываний методов eof(), файл из двух байт "\n" или "0D0A" будет интерпретироваться как содержащий одну строку. Хотя там их две - одна пустая строка в 0 байт с терминатором CRLF и вторая пустая строка в 0 байт без терминатора.

UPD: не ленитесь писать свой строковый парсер!

Можно спросить, а почему такая мудреная реализация move_assign?:

template <class Ty> void move_assign(Ty *dest, Ty &&other)
{
    if (dest != &other) {
        dest->~Ty();
        new(dest)Ty(std::move(other));
    }
}

Почему нельзя было просто сделать *dest = std::move(other)?

Кроме того, если мне не изменяет склероз, то после того, как вы сделали placement new для dest, то для дальнейшего использования dest вам следует применить к нему std::launder.

Если тип объекта не поменялся (включая cv-квалификации), то std::launder не нужен.

Однако вопрос о том, к чему тут эта странная деструкуция-конструкция вместо обычного присваивания, действительно стоит.

Если тип объекта не поменялся (включая cv-квалификации), то std::launder не нужен.

Я так понимаю, что это начиная с C++20 std::launder не нужен. А в рамках C++17 вроде бы еще нужен, даже если тип остается тем же самым (здесь в конце раздела "std::launder and pointer values").

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

Спасибо, что нашли время ответить. Но мотивация к появлению move_assign не смотря на обилие текста от меня ускользнула.

Если функцию move_assign() "реализовать" как *dest = std::move(other);

Так ведь можно вообще не иметь move_assign и делать просто *dest = std::move(other) в местах, где вы применяете move_assign.

И нет, "Потому, что UniqueHandle не умеет корректно закрывать handle." -- не оправдание. Нафиг нужен UniqueHandle, который не умеет закрывать хранящийся в нем дескриптор.

Возможно, если не поленюсь, напишу proposal к стандарту C++

Может лучше пожалеть людей из комитета, которым придется читать подобный бред?

Так ведь можно вообще не иметь 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), а позволяет просто писать меньше кода, вот и всё.

Но, за неимением вашего кода, давайте покажу на примере кода от Microsoft.

Мне доводилось заглядывать в код C++ REST SDK, этого хватило, чтобы скептически относиться к "коду от Microsoft". Так что ссылка на такой себе авторитет.

Сам я предпочитаю реализовывать copy operator и move operator через идиому "make temporary then swap".

Но, что даже более важно, благодаря UniqueHandle можно добавлять в FileHandle новые переменные-класса только в одном месте — в самом классе, т.е. править код move assignment operator и move constructor при этом не нужно, т.к. новые переменные-класса будут учитываться автоматически в сгенерированном компилятором коде move assignment operator и move constructor.

Только вот если вашу реализацию использовать в рамках C++17, то там нужен std::launder, т.к. вызовом деструктора вы прекращаете лайфтайм старого объекта, а возвращенное оператором placement new значение выбрасываете.

Если вы считаете, что UniqueHandle не нужен

Вы невнимательно прочитали: я говорил, что UniqueHandle, который не может сделать close, не нужен. А если он может делать close, то очистка ресурсов в move operator не проблема.

желательно не просто на словах, а в виде конкретного кода

Где-то здесь есть реализация handle_holder.

Только вот если вашу реализацию использовать в рамках 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"? :)(:]

Для заменяемых типов (transparently replaceable) — не нужен. (Т.к. в данном случае имеет место реконструкция объекта того же самого типа, т.е. тип исходного объекта и вновь созданного полностью совпадают.)

У вас move_assign -- это шаблон и вы не знаете, есть ли в типе T константные поля или поля-ссылки. В статье, на которую вы сослались, соответствующая ситуация описана в разделе "Использование std::launder", где рассматривается структура с const int n внутри.

Проблема. Т.к. одного умения делать close, увы, не всегда достаточно. Конкретный пример: деструктор в классе OFile вызывает метод flush(), который записывает все оставшиеся данные в буфере в файл.

Не вижу причин, по которым ваш ~OFile не может быть реализован вот так:

~OFile() { close(); }

Следовательно, в условном Traits::destroy вы можете делать флуширование, если вам это нужно.

во-вторых, что более важно: при добавлении новых переменных-класса в OFile обновлять код реализации operator=(OFile &&) не потребуется.

Да уже понятно что у вас пунктик по этому поводу. Только вот если использовать идиому "make temporary then swap", то модифицировать придется реализацию swap, что все равно полезно.

А вот когда у объекта деструктор вызывается явно просто чтобы сделать присваивание/перемещение, то это добавит приключений тем программистам, которые при разработке добавляют в деструкторы своих классов отладочные печати.

Так что мой вопрос остаётся в силе: было бы интересно увидеть, какую альтернативу моему решению предлагаете вы?

Вам я ничего не предлагаю. Реализация, ссылку на которую я давал, у меня давно используется.

Не вижу причин, по которым ваш ~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 для меня остаётся загадкой.

Такое тоже будет работать, но что это даёт?

Следование принципу DRY. У вас, по сути, close и деструктор должны приводить к одинаковым результатам. Поэтому логичным выглядит не повторять логику в разных местах, а выражать одно через другое.

Кстати говоря, странно, почему вы оператор перемещения через move_assign делаете, а close -- нет. Можно же close сделать так:

void OFile::close() {
  move_assign(*this, OFile{});
}

И получить те же самые бенефиты, на которых вы зациклены: про добавлении нового поля в класс не нужно делать лишние операции.

Чтобы делать флуширование, необходим доступ к buffer и buffer_pos объекта OFile. Т.е. в ваш handle_holder помимо самого handle, необходимо добавить указатель на объект OFile, std::function или что-то вроде того.

Это потому, что вы увели разговор в сторону. Изначально речь шла о том, что я не вижу смысле в UniqueHandle, который не может сам закрыть хранящийся в нем дескриптор. И показал вам свою реализацию похожего класса.

Вы же сейчас пытаетесь перевести разговор на другой уровень абстракции, когда над дескриптором появляется еще какая-то логика (вроде промежуточных буферов). Очевидно, что handle_holder к этой логике никакого отношения не имеет и не следует в него закладывать еще что-то.

Но для этого более высокого уровня абстракции можно сделать свой класс, хранящий и handle_holder и еще что-то, и знающий как это что-то обработать при закрытии ресурса.

Вы так говорите, как будто это что-то плохое. :)(:

Оно выглядит как-то излишне экстремально.

И еще вопрос(ы) по исключениям:

Вы принципиально не наследуете свои исключения от std::exception (или какого-то наследника std::exception)?

Почему вы не используете дерево наследования для своих исключений? Вроде такого:

class FfhException {};
class FileOpenError : public FfhError {};
class WrongFileNameStr : public FfhError {};
class FileIsAlreadyOpened : public FfhError {};
class AttemptToReadAClosedFile : public FfhError {};

Без такой иерархии пользователи могут забабахаться ловить исключения из вашей библиотеки. Особенно если со временем вы туда еще какие-то исключения будете добавлять.

std::chrono::high_resolution_timer::now()

подловили. из-за невнимательности ткнул в несуществующий API

UFO just landed and posted this here

Более того, множество существующих в настоящее время HDD всё ещё имеют размер сектора 512 байт. У меня таких есть несколько (правда им уж несколько лет). А вот по поводу секторов с ssd/nvme это вообще неизвестная величина. Вполне может быть, что физические сектора там (минимальная единица записи в флешку) будет ну например 16 килобайт или больше. Но снаружи этого никогда не узнать (точнее, можно узнать лишь сильно косвенными методами), т.к. любой nvme/ssd -- это эмулятор блочного устройства с заданным кол-вом и размером блоков (секторов).

UFO just landed and posted this here

На всех современных устройствах, включая 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)

Неужели ваши диски более старые?

UFO just landed and posted this here

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

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

Device Model: ST9250315AS

Sector Size: 512 bytes logical/physical

Например.

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

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

Правда использую по назначению. Для перетаскивания инфы (засунув его в усб-коробку) и для организации файлопомоек.

А почему при чтении используется readline(), вместо потокового ввода? Это же c++? Логично сразу учить студентов корректно работать с потоковым вводом.

Скажите спасибо, что не getch. В следующей серии - пишем свой sscanf (полезно, я один раз написал) или fmt

А почему при чтении используется 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;
}

По теме статьи:

readline() читает текущую строку до символа конца строки. Т.е. если файл заканчивается пустой строкой, то readline() должен вернуть пустую строку. Если файл заканчивается ьремя пустыми строками, то readline () должен три раза вернуть пустую строку. Заглядывать на символ вперед, или искать конец файла после символа пустой строки readline не должен. Подумайте о случае, когда ваш файл - это com-port, например (/dev/serial0) . В этом случае символа конца файла вообще может не быть. И readline(), если попытается считать данные после пустой строки, просто зависнет, т.к. данные еще не пришли. И в этом случае вы всегда будете получать последнюю строку, только когда прийдет следующий символ.

ну вообще там конец передачи м.б. Либо пришел по кабелю, либо драйвер сгенерил, если строб пропал. А так это проблема на линкс и с дисковымифайламислучается

IFile это очень плохое имя, даже с учетом всех рассуждений в комментарии

и почему не используется отдельный namespace?

функции "прочитай и верни строку по значению" в стандартном API нет из соображений производительности, при работе в цикле данные вычитываются в "разогретую" и преалоцированную области памяти

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++ компилятор вполне может применить такую оптимизацию!

UFO just landed and posted this here

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

Послушайте. Текущее решение — это не первое, что пришло мне в голову, и что я сразу же бросился реализовывать. И это не первая реализация типа File в 11l. Это результат размышлений на протяжении нескольких лет.

И принимая данное решение я прежде всего исходил из соображений практичности. С задачами типа «его хэндл/дескриптор можно передать в другой процесс» типичный программист прикладного ПО за всю свою карьеру сталкивается примерно ни разу. Признайтесь честно, ведь и вам не приходилось сталкиваться с такой задачей на практике?

К тому же, тип File в 11l — это аналог std::fstream (а точнее, std::ifstream/std::ofstream) в C++. Вас же не смущает невозможность передать std::fstream в другой процесс?

Для обёртки вокруг низкоуровневого файлового дескриптора будет использоваться отдельный тип (вроде FileDescriptor/FileHandle или даже os:File), когда он понадобится. А короткое имя File гораздо больше смысла имеет оставить за реализацией, которая и требуется чаще всего при типичной работе с файлами (прочитать или записать что-то в файл).

Всего-то надо позвать функцию ОС и сделать это удобно.

Если файл читается/пишется не целиком, то «всего-то позвать функцию ОС» не достаточно, т.к. вызов функции ОС осуществляет переключение в Ring 0 и обратно, что очень дорого. Для эффективной работы без буферизации чтения/записи не обойтись.

обычный булевый флаг-параметр, который совсем не всегда константа

Если такое и потребуется, то лучше использовать отдельный тип для этого (IOFile например). Тип File в 11l ориентирован на типичную работу с файлами, причём эффективную (за счёт буферизации) и безопасную (за счёт проверок на этапе компиляции).

Перед тем, как обсуждать "правильность" поведения API, надо договориться о том, какие у нас "правильные" ожидания.

  • Сколько строк должно быть прочитано из пустого файла?

  • Сколько строк должно быть прочитано из файла, содержащего единственный байт 'a'?

  • Сколько строк должно быть прочитано из файла '\n'?

  • Сколько строк должно быть прочитано из файла 'a\n'?

  • Сколько строк должно быть прочитано из файла 'a\nb'?

И вот по этим ответам уже придумывать API.

надо договориться о том, какие у нас "правильные" ожидания.

Так ведь давно уже все договорились.

Ответы: 0, 1, 1, 1, 2.

Код чтения файла по строкам для языков Python, C++ и Rust я уже приводил в этом комментарии.
Только что проверил в Nim [взяв за основу этот код, а также используя lines] и в Go [на основе кода отсюда]. Результат такой же.

Отличие в разных языках только в том, включается ли в прочитанную строку символ \n или нет (и функция read_line() в ffh имеет опциональный аргумент keep_newline для выбора желаемого поведения).

Изначальный посыл статьи был в неочевидной работе метода eof() в C++. И неправильное количество прочитанных строк получалось именно из-за него.

Посмотрел по диагонали вашу библиотеку...

ssize_t r = ::read(fd, b, std::min(sz, (size_t)0x7ffff000)); // On Linux, read() will transfer at most 0x7ffff000 bytes
if (r == -1)
  throw IOError();

Вот здесь не все возвраты -1 будут ошибкой. Если в errno находится EINTR, то такую ситуацию надо обрабатывать как частичное чтение, которое не успело прочитать совсем ничего (0 байт).

Вот здесь не все возвраты -1 будут ошибкой. Если в errno находится EINTR, то такую ситуацию надо обрабатывать как частичное чтение

Насколько я понимаю, для обычных файлов такого произойти не может. Это только для "slow" devices (terminal, pipe, socket).
И ни в одной реализации стандартной библиотеки языка Си (для Linux и FreeBSD) я не увидел проверки на EINTR в коде fread/refill: везде в случае, когда read возвращает число < 0, сразу возвращается ошибка. Поправьте, если ошибаюсь.

которое не успело прочитать совсем ничего (0 байт).

Возврат 0 — это ведь всегда признак конца файла, а не "прочитано 0 байт". (Т.е. функция read никак не может проинформировать о том, что было успешно прочитано 0 байт.)

Схожую позицию разделяет Реймонд Чен, объясняя почему функция ReadFile() из WinAPI никогда не возвращает меньше байт, чем было запрошено, при условии, что конец файла не был достигнут.

ReadFile() запросто возвращает меньше чем запрошено.

Например, когда читается буфер консоли и она в режиме ENABLE_LINE_INPUT. Будет построчное чтение. При этом не помешает ещё и GetLastError() проверять каждый раз, так как за кадром может приехать Ctrl+C.

Далее, а что такое "конец файла" для pipe-ов ? Их чтение "разбивается" на куски операцией записи в pipe. И нормальное поведение в этом случае - продолжать читать, а не отваливаться с сообщением о "конце файла".

Таким образом, и на WinAPI, любой минимально обобщённый код на тему должен учитывать возможность частичного чтения.

Почему в C++ явный оператор преобразования к bool был сделан именно так? Для того, чтобы его можно было 

Все-таки, «почему – потому что» и «для чего – для того»

Sign up to leave a comment.

Articles