Search code examples
c++c++17boost-filesystemstd-filesystem

Why does std::filesystem provide so many non-member functions?


Consider for example file_size. To get the size of a file we will be using

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

Nothing wrong with that, if it were plain ol' C, but having been taught that C++ is an OO language [I do know it's multi-paradigm, apologies to our language lawyers :-)] that just feels so ... imperative (shudder) to me, where I have come to expect the object-ish

auto n = p.file_size();

instead. The same holds for other functions, such as resize_file, remove_file and probably more.

Do you know of any rationale why Boost and consequently std::filesystem chose this imperative style instead of the object-ish one? What is the benefit? Boost mentions the rule (at the very bottom), but no rationale for it.

I was thinking about inherent issues such as ps state after remove_file(p), or error flags (overloads with additional argument), but neither approach solves these less elegant than the other.


You can observe a similar pattern with iterators, where nowadays we can (are supposed to?) do begin(it) instead of it.begin(), but here I think the rationale was to be more in line with the non-modifying next(it) and such.


Solution

  • There are a couple of good answers already posted, but they do not get to the heart of the matter: all other things being equal, if you can implement something as a free, non-friend function, you always should.

    Why?

    Because, free, non-friend functions, do not have privileged access to state. Testing classes is much harder than testing functions because you have to convince yourself that the class' invariants are maintained no matter which members functions are called, or even combinations of member functions. The more member/friend functions you have, the more work you have to do.

    Free functions can be reasoned about and tested standalone. Because they don't have privileged access to class state, they cannot possibly violate any class invariants.

    I don't know the details of what invariants and what privileged access path allows, but obviously they were able to implement a lot of functionality as free functions, and they make the right choice and did so.

    Scott Meyers brilliant article on this topic, giving the "algorithm" for whether to make a function a member or not.

    Here's Herb Sutter bemoaning the massive interface of std::string. Why? Because, much of string's interface could have been implemented as free functions. It may be a bit more unwieldy to use on occasion, but it's easier to test, reason about, improves encapsulation and modularity, opens opportunities up for code reuse that were not there before, etc.