Почему std:: filesystem предоставляет так много функций, не являющихся членами?

рассмотрим, например file_size. Чтобы получить размер файла, мы будем использовать

std::filesystem::path p = std::filesystem::current_path();
// ... usual "does this exist && is this a file" boilerplate
auto n = std::filesystem::file_size(p);

в этом нет ничего плохого, если бы это был простой Ol' C, но его учили, что C++-это язык OO [я знаю, что это многопарадигма, извинения нашим языковым юристам: -)], который просто чувствует себя так ... важно (вздрагивает) ко мне, где я ожидал увидеть объект-иш

auto n = p.file_size();
. То же самое относится и к другим функциям, таким как resize_file, remove_file и, наверное, больше.

знаете ли вы о каком-либо обосновании, почему Boost и, следовательно,std::filesystem выбрал этот императивный стиль вместо объектного? В чем выгода? Boost упоминает правило (в самом низу), но никаких оснований для этого.

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


вы можете наблюдать аналогичную картину с итераторами, где в настоящее время мы можем (должны?) do begin(it) вместо it.begin(), но здесь я думаю, что обоснование должно было быть больше в соответствии с немодифицируемые next(it) и такие.

3 ответов


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

почему?

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

свободные функции можно рассуждать о и протестировать автономно. Поскольку они не имеют привилегированного доступа к состоянию класса, они не могут нарушать какие-либо инварианты класса.

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

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

здесь Херб Саттер оплакивает массивный интерфейс std::string. Почему? Потому что, многое из может быть реализован как свободные функции. Он может быть немного более громоздким в использовании иногда, но его легче проверить, рассуждать, улучшает инкапсуляция и модульность, открывает возможности для повторного использования кода, которых раньше не было и т. д.


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

вы даже указали на объяснение этого:

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

вот в чем причина.

теоретически возможно использовать filesystem::path в системе нет дисков. The path класс просто содержит строку символов и позволяет манипулировать этой строкой, преобразовывая между наборами символов и используя некоторые правила, определяющие структуру имен файлов и путей в ОС хоста. Например, он знает, что имена каталогов разделяются / на системах POSIX и by \ на Windows. Манипулируя краю в path является "лексической операцией", потому что она просто выполняет манипуляции со строками.

функции, не являющиеся членами, которые известны как "операции файловой системы", совершенно разные. Они не просто работают с абстрактным path объект, который является просто строкой символов, они совершают операций ввода/вывода, доступ к файловой системе ( не является членом path, потому что это не путь. Путь-это просто представление имени файла, а не фактического файла. Функция file_size ищет физический файл с заданным именем и пытается прочитать его размера. Это не свойство файла имя, это свойство постоянного файла в файловой системе. То, что существует совершенно отдельно от строки символов в памяти, которая содержит имя файл.

другими словами, я могу иметь path объект, который содержит полную ерунду, как filesystem::path p("hgkugkkgkuegakugnkunfkw") и это нормально. Я могу добавить к этому пути или спросить, есть ли у него корневой каталог и т. д. Но я не могу прочитать размер такого файла, если он не существует. У меня может быть путь к файлам, которые существуют, но у меня нет разрешения на доступ, например filesystem::path p("/root/secret_admin_files.txt"); и это тоже хорошо, потому что это просто строка символов. Я бы только получил ошибку "отказано в разрешении", когда я пытался получить доступ к чему-то в этом месте с помощью функции файловой системы.

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

вы можете наблюдать аналогичную картину с итераторами, где в настоящее время мы можем (должны?) начать делать(это) вместо этого.begin (), но здесь я думаю, что обоснование должно было быть больше в соответствии с не изменяющимся next (it) и такие.

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

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

кроме того, есть вещи, которые вы не можете сделать, когда все является функцией-членом:

struct ConvertibleToPath {
  operator const std::filesystem::path& () const;
  // ...
};

ConvertibleToPath c;
auto n = std::filesystem::file_size(c);  // works fine

но если file_size являлся членом path:

c.file_size();   // wouldn't work
static_cast<const std::filesystem::path&>(c).file_size(); // yay, feels object-ish!

несколько причин (несколько спекулятивных, хотя я не очень внимательно слежу за процессом стандартизации):

  1. потому что он основан на boost::filesystem, который разработан таким образом. Теперь вы можете спросить: "почему boost::filesystem разработан таким образом?", что было бы справедливым вопросом, но учитывая, что это было, и что он видел много пробега так, как он есть, он был принят в стандарт с очень небольшими изменениями. Так были некоторые другие конструкции Boost (хотя иногда есть некоторые изменения, в основном под капотом).

  2. общим принципом при проектировании классов является "если функция не нуждается в доступе к защищенным / закрытым членам класса и может вместо этого использовать существующие члены - вы также не делаете ее членом."Хотя не все это приписывают-кажется, проектировщики boost::filesystem do.

    см. Обсуждение (и аргумент для) этого в контексте std::string(), класс "монолит" с множеством методов, c++ luminary Hebert Саттер, в гуру недели #84.

  3. ожидалось, что в C++17 у нас уже может быть единый синтаксис вызова (см. Stroustrup Bjarneпредложение). Если бы это было принято в стандарт, призывая

    p.file_size();
    

    было бы эквивалентно вызову

    file_size(p);
    

    таким образом, вы могли бы выбрать все, что вам нравится. В основном.