Как-то раз Бобу поручили построчно обработать текстовый файл. Боб решил решить эту задачу на C++, так как известно, что мало найдётся языков, которые могли бы потягаться с C++ в скорости. Поскольку C++ для Боба — дело новое, неосвоенное, он решил погуглить спросить ChatGPT, какой способ построчного считывания файла на C++. Для этого потребовалось немного затравочного кода, зато не пришлось пролистывать бесконечные страницы документации по стандартной библиотеке C++.

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

?

После этого Боб выложил окончательную версию кода на GitHub в файле TextFileReader.h, и вы смело можете использовать его в ваших проектах.

Чтение строк - решение от ChatGPT

Когда Bob спросил ChatGPT, каков наиболее популярный подход для построчного чтения файла на C++, искусственный интеллект предложил решение на основе функции std::getline() из стандартной библиотеки.

Вот как именно он выглядит

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

int main() {
    std::ifstream file("example.txt"); // открыть файл

    if (file.is_open()) { // проверить, успешно ли открылся файл
        std::string line;
        while (std::getline(file, line)) { // прочитать все строки в файле, одну за другой
            std::cout << line << std::endl; // вывести строку
        }
        file.close(); // когда дело будет сделано — закрыть файл
    } else {
        // вывести сообщение об ошибке, если файл открыть не удастся
        std::cerr << "Unable to open the file." << std::endl;
    }

    return 0;
}

Версия ChatGPT, решение задачи «как прочитать файл на C++ строку за строкой?»

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

Чтение строк - аппетитное решение

Боб обнаружил, что в C++ можно обойтись без константного объекта std::string, изменять который он не собирался, можно вернуть std::string_view — легковесный объект, который был впервые введён в C++17 ради того, чтобы обойтись без копирования константных строк. Внутрисистемно std::string_view занимает всего 16 байт и содержит два поля-экземпляра:

  • указатель на строку

  • размер строки

На следующее утро (перед сном наш герой лишний раз обдумал задачу) у Боба сложился конкретный план действий, который формулируется примерно так:

Обратите внимание, как тщательно Боб обрабатывает неполные строки в конце буфера. Эти тонкости Боб отложил на потом. Пока Боба больше волнует, как его классом будут пользоваться коллеги.

Поток задач

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

Боб решил не изобретать велосипед, а придерживаться распространённого паттерна, который ему предложил ChatGPT. Этот подход строится так:

TextFileReader src;
if (Error err = 1️⃣ src.open("stocks_20240310.csv"); !err.empty())
  return err;

Error err;
while (2️⃣ src.readline(err)) {
  //  Обработать 3️⃣ src.line()
}
if (!err.empty())
  return err;

Построчная обработка текстового файла на C++

1️⃣ open() Обычно, зная путь к файлу, мы для начала пробуем его открыть. По тем или иным причинам этот шаг может не удаться, и Боб решил, что это следует обозначить. Если случится проблема, то мы вернём строку с описанием ситуации, например, «File doesn't exist» (Файл не существует) или «Too many open files» (Открыто слишком много файлов). Это не идеальный способ указать на ошибку, но пока его вполне достаточно.

2️⃣ Цикл readline() идентичен std::getline() за тем исключением, что, когда мы обращаемся к актуальной строке при помощи 3️⃣line(), и она возвращает std::string_view во внутренний буфер. Операция эффективна, поскольку она не связана ни с каким копированием.

Боб также счёл, что будет полезно, если пользователи смогут считывать файл фиксированными фрагментами, не деля их на строки. Именно поэтому Боб предложил альтернативу для этапов 2️⃣ и 3️⃣, которая выглядит так:

Error err;
while (4️⃣ src.read(err)) {
  // обработать 5️⃣ src.buf();
}

Обрабатываем текстовый файл на C++ фиксированными фрагментами

4️⃣ read() забирает следующий фрагмент данных и помещает его во внутренний буфер. Содержимое этого буфера можно прочитать при помощи метода 5️⃣TextFileReader::buf() , который возвращает легковесное строковое представление, и это происходит без копирования.

Подробно о TextFileReader

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

Ключевые методы

Метод readline() — это центральный элемент всего класса. Он определяет, какие ещё методы и члены данных должны содержаться в классе. Интуитивно Бобу понято, что любой метод будет бесполезен, пока он не доведёт readline() до готовности.

Потратив примерно час на прототипирование, Боб обернул все операции в следующую функцию, которая состоит из пяти этапов:

bool
TextFileReader::readline(Error& err)
{
  do {
    // 1️⃣ Найти следующий символ \n 
    if (const auto p = (c8*)memchr(m_buf + m_cursor, '\n', m_size - m_cursor); p) {
      m_line = string_view(m_buf + m_cursor, p - (m_buf + m_cursor));
      m_cursor = p - m_buf + 1;
      return true;
    }

    // 2️⃣ Символ \n не найден
    if (std::feof(m_src)) {
      m_line = string_view(m_buf + m_cursor, m_size - m_cursor);
      return m_cursor < m_size;
    }

    // 3️⃣ Скопировать хвостовую часть в начало
    if (m_cursor < m_size) {
      std::memcpy(m_buf, m_buf + m_cursor, m_size - m_cursor);
    }

    // 4️⃣ Потребить ещё больше данных (не обновлять m_cursor)
    if (err = read(m_size - m_cursor); !err.empty()) {
      err = "TextFileReader::readline : " + err;
      return false;
    }
  } while (m_size);

  // 5️⃣ Вероятно, у нас уже должно быть возвращено false в ❷ (что означает «конец файла»)
  err = "TextFileReader::readline : EOF";
  return false;
};

read() — это вспомогательный метод, считывающий фрагменты текстового файла во внутренний буфер. Боб реализовал его следующим образом:

bool
TextFileReader::read(Error& err, u64 pos = 0)
{
  if (m_size = std::fread(m_buf + pos, 1, m_capacity - pos, m_src); !m_size) {
    if (std::ferror(m_src)) {
      err = "Fail to read : errno = " + std::to_string(m_size);
      return false
    }
  }

  m_size += pos;
  m_cursor = 0;

  return true;
};

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

?

Запрещено одновременно использовать read() и readline(). После инициализации TextFileReader следует вызывать только один из них.

Вспомогательные методы

Оставшиеся методы отвечают за управление ресурсов: объект FILE и внутренний буфер памяти, которую нужно выделять и возвращать обратно. Именно это и делают методы open() и close().

Вызов close() опционален. Он будет автоматически вызываться деструктором метода open(), который для считывания нового файла повторно использует уже выделенный буфер. Так класс становится безопаснее, и при этом им удобнее пользоваться. Это отличный пример идиомы RAII на практике.

Алиса — более опытная коллега Боба — предложила ему отключить работающий по умолчанию конструктор копирования (или реализовать собственный, но добавила, что для данной задачи это многовато). Сказала, что работающий по умолчанию конструктор копирования дублирует указатели, помещаемые во внутренний буфер, но не сам буфер. В итоге из-за этого повреждается внутреннее состояние класса, когда продублированные читатели заполняют внутренние буферы информацией из одного и того же объекта FILE.

Бенчмарки

Бобу стало любопытно, как его код смотрится по сравнению с подходом std::getline, предложенным ChatGPT. Боб создал набор файлов с длиной строки от 10 до 1000 и количеством строк от 1000 до 1000000.

Удивительно, но его код оказался не менее чем в 20 раз быстрее std::getline, а иногда даже в 180 раз быстрее. «Неплохо для любителя!» — воскликнул он, гордясь собой.

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

Заключение

Главный вывод из этой истории — не в том, что файлы можно считывать в 100 раз быстрее (конечно, и это тоже важно). Самое важное, что нужно прорабатывать собственные решения даже для очень простых задач, как минимум, размышлять, а возможно ли такое решение. Именно так и учишься эффективнее всего — на практике. Можно вызубрить стандартную библиотеку C++, но никогда не заучишь умения быть инженером. Инженерия – это практика, и, чтобы преуспеть в ней, ею нужно заниматься, даже, если поначалу у вас будет получаться не слишком эффективно. Рано или поздно вы набьёте руку, станете более результативно мыслить, и ваш код также улучшится.

Удачи в программировании!?

Комментарии (37)


  1. skovoroad
    26.05.2024 07:00
    +6

    1. Это же сравнения на релизной сборке? Ну которые в 180 раз быстрее? Было бы неплохо Бобу пробежаться с профайлером и понять, откуда такая чудовищная разница, для одних только аллокаций что-то многовато.

    2. Скажем так - стиль требует полировки, если мы говорим о С++. Хаотичная и не идиоматичная обработка ошибок, отказ от raii, приведения типов в стиле Си, какие-то избыточные алиасы, пропавшие конструкторы и много такой фигни.

    3. Если говорить о дизайне, по существу буфер и ридер надо разделять, иначе нулевая гибкость.

    4. А так отличное упражнение для Боба, удачи ему.


  1. Rustified
    26.05.2024 07:00
    +21

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


    1. kbnrjlvfrfrf
      26.05.2024 07:00

      Да! Преждевременная оптимизация хуже преждевременной эякуляции!


  1. kbnrjlvfrfrf
    26.05.2024 07:00
    +4

    Именно так и учишься эффективнее всего — на практике. Можно вызубрить стандартную библиотеку C++, но никогда не заучишь умения быть инженером. Инженерия – это практика,

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


    1. SIISII
      26.05.2024 07:00

      Но такое решение уже будет непереносимым, так как полагается на наличие у ОС такой возможности. А её может и не быть -- скажем, если программе придётся работать на микроконтроллере, где виртуальной памяти в принципе не существует.


      1. kbnrjlvfrfrf
        26.05.2024 07:00
        +4

        Максимально эффективное решение в принципе и не может быть переносимым.


        1. SIISII
          26.05.2024 07:00

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


      1. vadimr
        26.05.2024 07:00

        На микроконтроллере, скажем, и файловой системы может не быть. А на мейнфрейме, как Вам хорошо известно, файл может не быть потоком. Сама постановка задачи уже не вполне переносима.

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


    1. klirichek
      26.05.2024 07:00

      Насчёт ммапинга - таки лучше снять розовые очки. То, что в юзерспейсе оно выглядит очень простым - совсем не значит, что оно такое же "под капотом". Отдельно выделенный поток ядра будет "там" считывать файл кусками, класть его в кэш и создавать иллюзию целостной бесшовной работы. Разные madvise может и помогут - но всё равно прямого доступа нет, есть опосредованный через кэш. Т.е. данные ВСË РАВНО аллоцируются, пусть и не в рамках процесса, а в ядре.
      Статья по запросу "Are You Sure You Want to Use MMAP in Your Database Management System" ещё больше развеет веру в маппинг.
      Вкратце - он не про быстродействие, а про простоту кода в юзерспейсе.


      1. kbnrjlvfrfrfrf
        26.05.2024 07:00

        То, что в юзерспейсе оно выглядит очень простым - совсем не значит, что оно такое же "под капотом".

        Согласен. Век живи век учись. Но я вырос в эпоху механических HDD, поэтому всё что прикасалось к I/O (в т.ч. сетевому) априори было бессмысленно оптимизировать. Маппинг действительно был скорее про простоту парсинга контента в случаях затруднительности весь файл сразу считать.


      1. rPman
        26.05.2024 07:00
        +3

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

        Данные не нужно копировать! они будут скопированы однократно с диска в память и собственно все, они останутся в оперативной памяти (кеше файловой системы) даже если приложение будет закрыто и запущенно снова. И это нереально полезно и удобно.

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

        Hidden text

        Пример с приложением, которое активно использует память - llama.cpp, с отключенным mlock вся языковая модель копируется в память, когда как с использованием маппинга памяти повторный запуск не тратит времени на загрузку модели, мало того, если часть модели будет вытеснена из кеша каким-либо приложением, с диска будет загружена только эта отсутствующая часть. Да, без дополнительных телодвижений не получится использовать эффективно приложение если памяти не хватает 'чуть чуть' (например модель требует 64gb оперативки но ее всего 64, а порядка еще гигабайт требует операционная система) просто потому что кеш так работает, он будет вытеснять старые данные из кеша в первую очередь вынуждая к перезагрузке всей модели а не только той маленькой части которая не влезла, но это решаемо достаточно простым кодом.


  1. sergio_nsk
    26.05.2024 07:00

    Что такое "аппетитное решение", можно растолковать? Оно вызывает голод?


    1. DSRussell
      26.05.2024 07:00
      +1

      Вызывает желание его попробовать


  1. Wesha
    26.05.2024 07:00
    +10

    Если всё настолько плохо, что хочется избежать копирования, то mmap() должно спасти отца русской демократии.


    1. rPman
      26.05.2024 07:00

      полностью согласен, но mmap есть только на x86 (unix/linux и windows) и arm, а к примеру если у тебя микроконтроллер, работу с файлами придется вести 'по старинке', потому что возможность отображения в адресное пространство чего то сгенерированного кодом (типа файла с диска) это аппаратная фича.


      1. kbnrjlvfrfrf
        26.05.2024 07:00

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


        1. rPman
          26.05.2024 07:00

          Да, для микроконтроллеров mmap не нужен.

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

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


          1. kbnrjlvfrfrf
            26.05.2024 07:00
            +1

            Для этого нужно прочитать конец файла,

            А просто продолжать держать в памяти последнюю записанную строку не судьба?


            1. Wesha
              26.05.2024 07:00
              +1

              Если пишем на любой блоковый носитель, то последний блок придётся читать хоть тушкой, хоть чучелом, потому что он [почти] при любом раскладе не до конца заполненный, и придётся в прочитанные данные новую строку (или как минимум её начало) добавить и записать изменённый блок обратно


              1. kbnrjlvfrfrf
                26.05.2024 07:00

                Задача выше ставилась про недопущение дупликатов строк. Но даже в случае ручного управления своей псевдофс, что мешает удерживать в памяти этот последний блок, добивать новую строку в конец и сбрасывать блок на носитель? Зачем его считывать опять? Надеюсь никто не додумается "а вдруг у нас 2 процесса в один файл пишут"? Хотя в случае такого идиотского архитектурного решения даже считывание уже не поможет.


                1. rPman
                  26.05.2024 07:00

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


                1. Wesha
                  26.05.2024 07:00

                  что мешает удерживать в памяти этот последний блок, добивать новую строку в конец и сбрасывать блок на носитель? Зачем его считывать опять?

                  Например, что если пропало питание, бандура стартовала с нуля — и, соответственно, в памяти ничего нет?

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


                  1. kbnrjlvfrfrfrf
                    26.05.2024 07:00

                    Например, что если пропало питание, бандура стартовала с нуля — и, соответственно, в памяти ничего нет?

                    Ну вот и прочитаем единожды в рамках процесса инициализации. Зачем это делать каждый раз? Страсть как любим экономить на спичках, но не замечаем что roundtrip к внешнему носителю как правило весьма неторопливое действие.

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

                    Да нет никаких там требований. Обычная учебная задача в сферическом вакууме. В реале обойдутся привычным getline, потому что всё равно далее по обработке куда более серьёзные тормоза.


                    1. Wesha
                      26.05.2024 07:00

                      > Например, что если 

                      Ну вот и

                      [...текли минуты, превращаясь в часы...]


  1. vadimr
    26.05.2024 07:00
    +6

    Вообще бессмысленные действия без учёта того, что и как мы дальше собираемся делать с этими строками.

    Чувак тут экономит на спичках на конструкторах (вместо чего, как верно замечено выше, лучше бы использовал mmap), а потом в миллион раз больше просадит на каких-нибудь регекспах.


    1. rPman
      26.05.2024 07:00

      .


  1. zubrbonasus
    26.05.2024 07:00
    +1

    Для Боба важно понять принцип YAGNI (You aren't gonna need it).

    Так, если разрабатывается бухгалтерское приложение, экономия памяти и времени "на спичках" не имеет никакого смысла.

    В тоже самое время, если разрабатывается Core серверного решения с прицелом на высокую нагрузку, тогда "каждая спичка на особом счету".

    Но! Если "каждая спичка на особом счету", тогда нужно кодить, обращаясь к памяти используя указатели, а не высокоуровневые функции.

    Удачи, Роберт Мартин младший!


  1. Kahelman
    26.05.2024 07:00
    +1

    Стандартная библиотека содержит функцию FGETS, совершен- но аналогичную функции GETLINE, которую мы использовали на всем протяжении книги. В результате обращения FGETS(LINE, MAXLINE, FP) следующая строка ввода (включая символ новой строки) считы- вается из файла FP в символьный массив LINE; самое большое MAXLINE_1 символ будет прочитан. Результирующая строка за- канчивается символом \ 0. Нормально функция FGETS возвращает LINE; в конце файла она возвращает NULL. (Наша функция GETLINE возвращает длину строки, а при выходе на конец файла нуль). Предназначенная для вывода функция FPUTS записывает строку (которая не обязана содержать символ новой строки) в файл: FPUTS(LINE, FP) Чтобы показать, что в функциях типа FGETS и FPUTS нет ничего таинственного, мы приводим их ниже, скопированными непосредственно из стандартной библиотеки ввода-вывода: #INCLUDE <STDIO.H> CHAR *FGETS(S,N,IOP) /GET AT MOST N CHARS FROM IOP/ CHAR *S; INT N; REGISTER FILE *IOP; ( REGISTER INT C; REGISTER CHAR *CS; CS = S; WHILE(--N>0&&(C=GETC(IOP)) !=EOF) IF ((*CS++ = C)=='\N') BREAK; *CS = '\0'; RETURN((C==EOF && CS==S) 7 NULL : S); ) FPUTS(S,IOP) /PUT STRING S ON FILS IOP/ REGISTER CHAR *S; REGISTER FILE *IOP; ( REGISTER INT C; WHILE (C = *S++) PUTC(C,IOP); )

    Керниган, Ричи. Язык программирования С

    Не благодарите.

    Пример как писать нормальный интерфейс.


    1. UranusExplorer
      26.05.2024 07:00

      То, что является "нормальным интерфейсом" в Си, не факт что будет являться нормальным интерфейсом в Си++, особенно в современном. Разные возможности у языков, разные сложившиеся подходы, разные best practises. Как уже отметили выше, в чистом Си, в отличие от плюсов, нет RAII, нет исключений, и т.д.


      1. Kahelman
        26.05.2024 07:00

        Вышеприведенный код не призыв переписать все на С. А пример реализации такого же метода написанный давным давно людьми которые понимаю «в колбасных обрезках» сравните их метод и то что нагромоздили в статье.

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


        1. kbnrjlvfrfrf
          26.05.2024 07:00

          прикрываясь заявлением о том что стандартный слишком медленный.

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


        1. UranusExplorer
          26.05.2024 07:00

          Так подождите, вы говорите про интерфейс или про реализацию? На "медленность" кода влияет именно реализация, у меня к реализации автора есть вопросы, но она в любом случае будет скрыта в своем .cpp-файле, и на то насколько удобно будет использовать в коде этот класс сложность реализации под капотом мало влияет.

          Если речь всё-таки про интерфейс, то это тот самый случай, когда вредно апеллировать к авторитетам, потому что интерфейс у вами рекомендуемого fgets откровенно плохой. Мы дёрнули fgets, нам вернулся null. Что это: мы дошли до конца файла или произошла ошибка ввода-вывода? Мы не знаем, надо дополнительно дергать feof() и ferror(). Более того, в случае I/O-ошибки нам может вернуться корректный указатель, но с некорректными данными, и ferror() вызывать все равно обязательно. В итоге как раз и получается монструозный код, где после каждого вызова функции чтения надо выполнять по несколько проверок. Далее, мы дёрнули fgets, проверили ferror, нас нет ошибки, вернулся указатель. Как понять - мы прочитали строку до конца, или мы прочитали до максимально допустимой длины (maxline), но не достигли конца строки? А никак. Даже пересчитать количество считанных символов в буфере до нуль-терминатора (что уже неэффективно с точки зрения производительности) не поможет, если строка оказалась длиной ровно maxline.

          А что до интерфейса автора статьи, он у него почти не уступает классической связке std::fstream + std::getline из стандартной библиотеки C++, и уж точно проще в использовании чем ваш fgets. Никакой монструозности, один метод чтобы открыть файл, один чтобы прочитать строку, один чтобы получить view прочитанной строки. Вполне простой и понятный интерфейс.

          Я бы, конечно, сделал чуть-чуть по-другому. Я бы возвращал std::optional<std::string_view> (nullopt если eof, либо сама строка), а при ошибках кидал исключения. Если по каким-то причинам исключения не используются, что лучше сделать монадический подход с чем-то типа std::expected, с ним вообще код будет очень красивый и лаконичный.

          А вот за что действительно автора надо очень долго и больно бить палкой - за using в глобальном неймспейсе в .h-файле. Так нельзя.


    1. Wesha
      26.05.2024 07:00
      +3

      Пример как писать нормальный интерфейс.

      ... а также как писать комментарий на хабре так, что его чёрта с два прочитаешь.


  1. kmatveev
    26.05.2024 07:00

    Я правильно понимаю, что та память, на которую указывает этот string_view, может быть перезатёрта в последующих вызовах readline(), то есть строку нужно или сразу анализировать, или сразу копировать?


    1. UranusExplorer
      26.05.2024 07:00

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


      1. Kahelman
        26.05.2024 07:00

        И чем ваш код тогда лучше стандартной функции C? Даже не С++?


        1. UranusExplorer
          26.05.2024 07:00

          Какой именно мой код? Я никакого кода тут не проводил.