Рассмотрим для примера
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);
Ничего плохого в этом нет, если бы это было просто C, но когда меня учили, что C ++ — это ОО-язык [я знаю, что это мульти-парадигма, извиняюсь перед нашими языковыми адвокатами :-)], который просто чувствует себя так … императив (вздрагивает) мне, где я пришел, чтобы ожидать объект-иш
auto n = p.file_size();
вместо. То же самое относится и к другим функциям, таким как resize_file
, remove_file
и, вероятно, больше.
Знаете ли вы какое-либо обоснование, почему Boost и, следовательно, std::filesystem
выбрал этот императивный стиль вместо объектного? В чем выгода? Увеличение упоминает правило (в самом низу), но без объяснения причин.
Я думал о неотъемлемых проблемах, таких как p
состояние после remove_file(p)
или флаги ошибок (перегрузки с дополнительным аргументом), но ни один подход не решает их менее элегантно, чем другой.
Вы можете наблюдать аналогичную модель с итераторами, где в настоящее время мы можем (должны?) Делать begin(it)
вместо it.begin()
, но здесь я думаю, что обоснование должно было больше соответствовать немодифицирующей next(it)
и тому подобное.
Есть несколько хороших ответов, которые уже опубликованы, но они не доходят до сути вопроса: при прочих равных условиях, если вы можете реализовать что-то как бесплатную функцию, не являющуюся другом, вы всегда должны это делать.
Зачем?
Потому что бесплатные, не дружественные функции, не имеют привилегированного доступа к состоянию. Тестирование классов намного сложнее, чем тестирование функций, потому что вы должны убедить себя, что инварианты класса поддерживаются независимо от того, какие функции-члены вызываются или даже комбинации функций-членов. Чем больше у вас функций члена / друга, тем больше работы вы должны выполнить.
Бесплатные функции могут быть аргументированы и протестированы отдельно. Поскольку у них нет привилегированного доступа к состоянию класса, они не могут нарушать инварианты классов.
Я не знаю деталей того, что инварианты и какой привилегированный доступ path
позволяет, но, очевидно, они смогли реализовать множество функций в качестве бесплатных функций, и они сделали правильный выбор и сделали это.
Скотт Майерс блестящая статья на эту тему, давая «алгоритм» для того, чтобы сделать функцию членом или нет.
Вот Херб Саттер оплакивает массивный интерфейс std::string
. Зачем? Потому что большая часть string
интерфейс мог быть реализован как свободные функции. Иногда это может быть немного громоздким для использования, но его легче тестировать, рассуждать, улучшить инкапсуляцию и модульность, открыть возможности для повторного использования кода, которых раньше не было, и т. Д.
Библиотека файловой системы имеет очень четкое разделение между filesystem::path
тип, представляющий абстрактное имя пути (которое даже не является именем существующего файла) и операции, которые обращаются к реальной физической файловой системе, то есть чтение + запись данных на диски.
Вы даже указали на объяснение этого:
Правило разработки состоит в том, что чисто лексические операции предоставляются как функции-члены пути к классам, а операции, выполняемые операционной системой, предоставляются как свободные функции.
Это причина.
Теоретически возможно использовать filesystem::path
в системе без дисков. path
Класс просто содержит строку символов и позволяет манипулировать этой строкой, конвертировать между наборами символов и использовать некоторые правила, которые определяют структуру имен файлов и путей в операционной системе хоста. Например, он знает, что имена каталогов разделены /
в системах POSIX и \
на винде. Манипулирование строкой в path
это «лексическая операция», потому что она просто выполняет манипуляции со строками.
Функции, не являющиеся членами, которые известны как «операции с файловой системой», совершенно разные. Они не просто работают с абстрактным path
объект, который является просто строкой символов, они выполняют фактические операции ввода-вывода, которые обращаются к файловой системе (stat
системные вызовы, open
, readdir
так далее.). Эти операции занимают path
аргумент, который называет файлы или каталоги для работы, а затем они получают доступ к реальным файлам или каталогам. Они не просто манипулируют строками в памяти.
Эти операции зависят от API, предоставляемого ОС для доступа к файлам, и от оборудования, которое может совершенно не работать при обработке строк в памяти. Диски могут быть переполнены или могут быть отключены до завершения операции, или могут быть аппаратные сбои.
Посмотрел на это, конечно file_size
не является членом path
потому что это не имеет ничего общего с самим путем. Путь — это просто представление имени файла, а не фактического файла. Функция file_size
ищет физический файл с указанным именем и пытается прочитать его размер. Это не свойство файла название, это свойство постоянного файла в файловой системе. То, что существует полностью отдельно от строки символов в памяти, содержащей имя файла.
Другими словами, я могу иметь path
объект, который содержит полную чушь, как filesystem::path p("hgkugkkgkuegakugnkunfkw")
и это нормально. Я могу добавить к этому пути, или спросить, есть ли у него корневой каталог и т. Д. Но я не могу прочитать размер такого файла, если он не существует. У меня может быть путь к файлам, которые существуют, но у меня нет прав доступа, например filesystem::path p("/root/secret_admin_files.txt");
и это тоже хорошо, потому что это просто строка символов. Я получал ошибку «Отказано в доступе» только тогда, когда пытался получить доступ к чему-либо в этом месте, используя функции работы файловой системы.
Так как path
Функции-члены никогда не затрагивают файловую систему, они никогда не могут потерпеть неудачу из-за разрешений или несуществующих файлов. Это полезная гарантия.
Вы можете наблюдать аналогичную модель с итераторами, где в настоящее время мы можем (должны?) Начинать (это) вместо it.begin (), но здесь я думаю, что логическое обоснование должно было быть больше в соответствии с неизменяющим следующим (это) и такие.
Нет, это потому, что он одинаково хорошо работает с массивами (которые не могут иметь функции-члены) и типами классов. Если вы знаете, что с диапазоном вы имеете дело с контейнером, а не с массивом, тогда вы можете использовать x.begin()
но если вы пишете общий код и не знаете, контейнер это или массив, тогда std::begin(x)
работает в обоих случаях.
Причины обеих этих причин (структура файловой системы и функции доступа к диапазону, не относящемуся к членам) — это не какое-то предпочтение против OO, а по гораздо более разумным, практическим причинам. Это был бы плохой дизайн, чтобы основывать любой из них, потому что это чувствует лучше для некоторых людей, которые любят OO, или чувствуют себя лучше для людей, которые не любят OO.
Кроме того, есть вещи, которые нельзя делать, когда все является функцией-членом:
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!
Несколько причин (хотя и несколько спекулятивных, я не очень внимательно слежу за процессом стандартизации):
Потому что это основано на boost::filesystem
, который разработан таким образом. Теперь вы можете спросить «Почему это boost::filesystem
спроектирован таким образом? «, что было бы справедливым вопросом, но, учитывая, что это было так, и что он видел большой пробег таким, какой он есть, он был принят в стандарт с очень небольшими изменениями. Так были и другие конструкции Boost ( хотя иногда происходят некоторые изменения, в основном под капотом).
Общим принципом при разработке классов является «если функция не нуждается в доступе к защищенным / закрытым членам класса и вместо этого может использовать существующие члены — вы также не делаете ее членом». Хотя не все приписывают это — кажется, дизайнеры boost::filesystem
делать.
Смотрите обсуждение (и аргумент) для этого в контексте std::string()
класс «монолит» с зиллионами методов Гуру недели № 84.
Ожидалось, что в C ++ 17 у нас уже может быть унифицированный синтаксис вызова (см. Бьюарна Страуструпа с высокой степенью читаемости предложение). Если это было принято в стандарт, позвонив
p.file_size();
было бы эквивалентно звонить
file_size(p);
так что вы могли бы выбрать все, что вам нравится. В принципе.
Просто в дополнение к тому, что другие уже заявили.
Одной из причин, почему люди недовольны подходом «без членов», является необходимость вводить std :: filesystem :: в начале API или использовать директивы using.
Но на самом деле вам не нужно, а просто пропустить пространство имен для вызова API следующим образом:
#include <iostream>
#include <filesystem>
int main()
{
auto p = std::filesystem::path{"/bin/cat"};
//notice file_size below has no namespace qualifiers
std::cout << "Binary size for your /bin/cat is " << file_size(p);
}
работает отлично, потому что имена функций также ищутся в пространствах имен их аргументов из-за ADL.
(живой образец https://wandbox.org/permlink/JrFz8FJG3OdgRwg9)