C++17 In Detail

06 May 2019

Converting from Boost to std::filesystem

Converting Boost to std::filesystem

As you may know std::filesystem evolved directly from Boost filesystem library. For a long time, it was available as a Technical Specification and later merged into C++17. Developers who used Boost can ask themselves what the differences between the two libs are. Can the code be easily converted to use std::filesystem? Today’s article is written by Scott Furry who writes about the differences he found when working with the two libs.

This article is a guest post from Scott Furry

Scott (see his Linkedin profile) is an independent software developer based in Edmonton, Alberta, Canada. He is a retired electronic technician from the Royal Canadian Navy. Over the years of work in civilian life, he has developed projects with many different languages for a variety of computer systems, including legacy architectures. He also contributions to and assists others using Open Source software.

Introduction

To the uninitiated, the Boost Libraries can seem very intimidating. I have often seen discussion threads where a user’s problem is answered with “use Boost”. There is a myriad of web pages filled with how-tos and advice on usage. My use case at the time years ago was quite simple. I wanted a cross-platform means to handle file paths. And that was my introduction, boost::filesystem library.

Unlike a large portion of the Boost ecosystem, boost::filesystem is not header-only. However, integration into a project was quite simple, and the functionality it provided was impressive. For the most part, a few extra keystrokes were needed to tell the compiler which shared objects to build against and where to find them. And into my personal skills toolkit it went.

I recently got it in my head to get back up to speed on C++ developments. There was a long stretch of time where I wasn’t fully using my coding skills, pounding away at the keyboard, bending bits to whims and will. Being stuck on C++11 ideas with C++20 looming somehow seemed wrong to me in many ways. I decided to take the time get acquainted with, at least, C++17 - the latest released standard.

While doing a deep dive on web articles about C++17, I tripped over the news that boost::filesystem had been merged into the C++17 standard.

Really?!?!?

blink. blink

I gotta try that!!!

Off I went cleaning off digital dust on old example code to see if the news was true or just hype. Seeing that the news was true, I documented my surprise with the change in a Reddit post. In exchanging comments with others, and with other experimentation, I came to understand that there are differences between the two implementations.

I’m not going to go on about “Do A. Do B. Do C. Easy.” when it comes to using std::filesystem. There are numerous other pages with content as if the reader has never seen this topic before. Instead, I am approaching the subject from the viewpoint the reader has some familiarity with boost::filesystem and may be looking to update the existing code to incorporate C++17.

Compiler Support

One major caveat is to ensure your compiler of choice is up to the task of using std::filesystem. None of this will work if the compiler is too old or has not implemented, at least experimentally, C++17 features. So, check your version now before making code changes.

The Filesystem Library (C++ technical specification ISO/IEC TS 18822:2015) was merged into the final release of C++17 in December 2017. In the two-plus years while C++17 was being assessed, std::filesystem was available as an experimental library.

GCC, in versions before 8.0, had users use the namespace

std::experimental::filesystem

This is no longer required in current 8.x releases. Exact details for GCC C++17 support can be found on the GNU CXX Status page.

LLVM states C++17 has been incorporated into Clang/LLVM since version 5.0. However, the implementation of std::filesystem was only available after the Clang/LLVM 7.0 release. See the LLVM CXX Status page for more details.

For those who use LLVM’s C++ library, see the Using Libcxx page for caveats about using std::filesystem. It basically boils down to ensuring you have LLVM Libcxx 9.0 installed. Note the name for the Libcxx Filesystem library, -lc++fs. You will need that for linking.

Visual Studio 2017 15.7 incorporated the full implementation of <filesystem>. Prior releases after VS2012 used the similar convention as GCC where uses had to invoke <experimental/filesystem>.

Article Code

For this article, I will reference two, almost identical, programs:

a) main_boostfs.cpp; and
b) main_stdfs.cpp.

The exact differences to the programs can be found in this diff patch.

These programs were developed to highlight commonality and differences in transitioning from boost::filesystem to std::filesystem.

Headers and Namespaces

For most uses, this is about the only real change that needs to be made to the code. You have to tell the compiler that you want to use the C++ Standard Filesystem Library. With boost::filesystem, code like:

#ifndef BOOST_FILESYSTEM_NO_DEPRECATED
#define BOOST_FILESYSTEM_NO_DEPRECATED
#endif

#include <boost/filesystem.hpp>
namespace fs = boost::filesystem;

now gets replaced with this:

#include <filesystem>
namespace fs = std::filesystem;

The #ifndef is not needed any more as we do not have to worry about deprecated boost::filesystem declarations. The #include directive is rather self-explanatory. And if, like me, you attempt to cut down the amount of typed code by using namespace aliases, swap out boost with std in the alias statement. The rest of the code should work as-is.

Compiling

To compile, the changes are equally straight forward. Without the need for the external Boost Filesystem library, we do not need to declare search directories for includes(-I) or linking(-L), if used.

A typical usage of boost::filesystem was to identify link libraries as:

CXX ... -lboost_system -lboost_filesystem

With std::filesystem, we only have to tell the compiler that C++17 is being used and to link against std::filesystem instead. The command becomes

CXX ... -std=c++17 ... -lstdc++fs

With the release of GCC 9.0, or bleeding edge latest release, there is no need for linkage to the external filesystem library. See GCC 9.0 Release Notes.

As noted earlier, when using LLVM’s Libcxx, the link library is -lc++fs.

For IDE users, check upstream if this is supported. You may need to explicitly change project settings to enable C++17, as well as std::filesystem.

For example, Visual Studio supports C++17 with the flags /std:c++17 or /std:c++latest set in

project options -> C/C++ -> Language -> C++ Language Standard

Eclipse, however, has not enabled C++17 support as of publication date.

If your project makes use of autotools or CMake, the needed changes are just as equally simple. If you’re only using boost::filesystem, you can remove the instructions entirely to search for the Boost libraries. Otherwise, just remove filesystem from the search for Boost.

Differences Found - System_Complete()

One issue that I tripped over rather quickly was code where I used the function boost::filesystem::system_complete().

I vaguely recalled that I found this function after web searches leading to Stack Overflow comments. I had written code to handle situations where I needed to pass a normalized path for that operating system to other functions. And then I just got into the habit of reusing the call in all my code.

After my edits of changing headers, the compile stopped with an error stating that the function could not be found in Standard C++. I could see it on the boost::filesystem API page, but not on any pages describing std::filesystem.

I think I found my solution after reading the description to the system_complete function on the boost::filesystem API page:

Effects: Composes an absolute path from p, using the same rules used by the
operating system to resolve a path passed as the filename argument to standard
library open functions.

The absolute() function does exist in std::filesystem. Depending on usage, the canonical() function could also be applied. After some further reading, it appears as both Boost and C++ Standard are going through some kind of collaborative revisions. This function, in its current form, may disappear in future. See the Version History section of the boost::filesystem front page.

Differences Found - Path Reverse Iterator

Another notable difference found between the two Filesystem implementations was with path iterators. For example, let’s say you are working on a game project. You start with a path to a map file, and it contains text that are the file names of images or music resources. You may jump to the thought of wrestling with the string mangling or even regex. That’s too hard an effort. There is an easier way to do this.

With the Filesystem library, you create a filesystem::path, passing in the location of an existing file, say the map file from the above hypothetical situation. A path iterator would then be used to walk up the directory tree, each iteration would produce the directory name found between the directory separators. The code could iterate up some number of directories to the resources root. From here, append paths back down into a folder, say the location of our game’s image or music resources. A reverse path iterator would be ideal in this situation to help break down, or decompose, the path without having to wrestle with directory separators for each platform.

Path iterator usage is shown in the example code for the article. At line 55 in main_boostfs.cpp, the parent to a user-supplied path is pulled apart with a reverse path iterator. The value of that iterator is then appended to another path variable.

fs::path revPath;
fs::path decompPath( testpath.parent_path() );
cout << "Decoposition: " << endl;
for( auto it = decompPath.rbegin(); it != decompPath.rend(); ++it )
{
    // (*it) is type fs::path
    cout << setw(6) << ' ' << (*it).string() << endl;
    // path append operator - separator added by library
    revPath /= (*it);
}
cout << "Reverse Path:" << setw(11) << ' ' << revPath.string() << endl;

We define decompPath to the value of the parent path passed in by the user. Creating another path variable is necessary since path::parent_path() cannot be used with path iterators. When executing the program, you pass in, for example, /home/me/somefolder/subfolder/article.txt. The parent path is /home/me/somefolder/subfolder. At the end of each iteration of the for loop, the path iterator value, whatever is found between director separators, is appended to the variable revPath. After executing this code, the expected output should be subfolder/somefolder/me/home/. The parent path backwards.

Problem is that std::filesystem does not implement a path reverse iterator, no path::rend() and path::rbegin(). There is only a forward iterator. It takes some creative manipulations to use forward iterators and go in reverse. In main_stdfs.cpp, at line 58, we do just that:

...
for(auto it = decompPath.end(); it != decompPath.begin();)
{
    --it;
    ...

We point an iterator to path::end(), decrement the iterator, and keep going only to stop when we reach the beginning iterator, path::begin(). The for loop step value is in the loop itself, not in the for loop line. Not a hard prospect but it does make the code appear awkward, in my opinion.

Differences Found - Append operator

This last difference was pointed out to me in an online discussion. There is some deviation in how each implementation handles the append operator, path::operator /=().

With boost::filesystem, the library will append whatever you give it. If the value to be appended begins with a directory separator, boost::filesystem will add a directory separator and whatever value you pass to append. A trim of any extra separators, along with any dot folders ./ or ../, can be done after calls to path::absolute(). Append means just that, append.

For std::filesystem, the library behaviour is similar to what a user experiences on the command line. As an analogy, doing

ls dir/subdir/

performs a directory listing on a path relative to the current working directory.
Executing

ls /dir/subdr/

means to list contents of the path starting from the root directory, otherwise a directory listing of an absolute path.

This is similar to how std::filesystem interprets appending values. Any path that starts with a directory separator, or /, is interpreted as meaning to append an absolute path. The path::operator /=() resets the variable to the value being appended, discarding previous contents.

This behaviour is highlighted in path decomposition in the article example code. Building up the path in reverse, the path iterator value on the last iteration is the root name, or / on Linux. Using boost::filesystem, the resulting value of revPath is exactly as one would expect from append operations, the parent path in reverse. Using std::filesystem, if we were to printout the value of revPath at the end of each iteration, we would see accumulation of appends. At the last append, the directory separator, indicating a root, is added to revPath. This last append resets revPath to what we pass to the operator, discarding the accumulated contents from previous iterations.

What this means for developers is that extra caution is needed in code. Developers will need to incorporate some validation or data checking to ensure that appended values do not start with a directory separator. All values that you intend to append must be relative paths.

Differences Caveat

There are indeed some differences between the two implementations. What is listed here is by no means a comprehensive listing. You may find others because of how you used boost::filesystem. Both boost and std::filesystem are evolving. You may find other differences not listed here.

Extra note: there’s one Stack Overflow questions with a conscience list of differences, so you might also review it: How similar are Boost filesystem and the standard C++ filesystem libraries? - Stack Overflow.

Conclusions

C++17 and std::filesystem are recent additions, relatively speaking. The virtual dust from implementation is still settling. Organizations behind operating systems, IDE’s, and compilers are still working on releases that fully implement all of the changes contained in C++17. Boost and the C++ Standard are both evolving.

With that being said, incorporating std::filesystem is rather straightforward for most use cases. If your development environment is up to date, and there is no, or minimal, impact to users or program behaviour then I would encourage the change to std::filesystem. Removing the dependency on an external library from a project, in my opinion, is boon for code maintenance. With improved C++17 integration into GCC 9.0, maintenance will get easier.

How about you? Have you used boost filesystem? do you plan to move to std::filesystem?

References

C++17 In Detail
© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.