File Handling in C++17 (Part-I) : Basic Concepts

Share the Article

File handling is a special topic in C++17. This is because, the C++17 file-related features were created by including the Boost Filesystem library. In this integration, some of the boost feature were improved, some were cleaned-up and then some were newly added. Therefore, the file topic became a very feature rich and interesting.

To use the new features, a program needs to use following header.

#include <filesystem>

Open an Existing File : File handling

The following example code uses some fundamental file handling functions and classes from C++ 17 filesystem library. example,

  • filesystem::path class
  • exists( )
  • is_regular_file( )
  • file_size( )

The last 3 functions are self-explanatory from the name. However, the “path” class is very special. For now, we just assume that it is an element representing the location where the file shall store. Although, there is much more behind this and this topic shall be discussed in Part 2 of FileSystem Series.

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace int main() { std::string pathfile = "myfile"; std::filesystem::path p {pathfile}; if (std::filesystem::exists(p)) { if (std::filesystem::is_regular_file(p)) cout << p << ": " << std::filesystem::file_size(p) << " bytes\n"; } else { std::cout << "path " << p << " does not exist\n"; } return 0; }

Output

output of code using filesystem library for simple file

Open and Iterate a directory

With minor changes, the above program can work for a directory too. i.e., by using is_directory( ). The function filesystem::is_directory( ) returns true or false. Further, the code below uses a directory_iterator to iterate all the elements present inside this directory. However, the only problem with this iterator is that it cannot recursively iterate the elements inside sub-hierarchies. This limitation shall be removed in by use of a special recursive iterator (after this example).

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace int main() { std::string pathfile = "mydir"; filesystem::path p {pathfile}; if (std::filesystem::exists(p)) { if (std::filesystem::is_directory(p)) { cout << p << " is a directory:\n"; for (auto& e : std::filesystem::directory_iterator(p)) { std::cout << " " << e.path() << '\n'; } } } else { std::cout << "path " << p << " does not exist\n"; } return 0; }

Output

The code shows that with directory_iterator( ), the program is not printing the file4. Although this is present in the sub-hierarchy.

output of code using directory_iteratory in filesystem library

Recursive Iterator

The above code can use a better iterator that shall iterate the sub-hierarchy in directory tree. When we replace the code in for-loop, it shall provide a better output.

for (auto& e : std::filesystem::recursive_directory_iterator(p)) { std::cout << " " << e.path() << '\n'; }

However, there is another problem. That is, by default, the recursive iterator shall not follow symbolic links. This means, if an element present in a directory hierarchy is a symlink to another directory, then only link-name shall get printed. The following snapshot shows that the symbolic link name “mydir/mylink” is printed. The program shall not deference this link.

output of code using recursive_directory_iterator in filesystem library

follow_directory_symlink (for dereference)

The recursive_iterator can accept a special option. This shall resolve the symlink problem.

auto options {std::filesystem::directory_options::follow_directory_symlink}; for (auto& e : std::filesystem::recursive_directory_iterator(p, options)) { std::cout << " " << e.path() << '\n'; }

Complete Example of Directory Iteration

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathDir = "mydir"; std::filesystem::path p {pathDir}; if (std::filesystem::exists(p)) { if (std::filesystem::is_directory(p)) { auto options {std::filesystem::directory_options::\ follow_directory_symlink }; cout << p << " is a directory:\n"; for (auto& e : std::filesystem::recursive_directory_iterator (p, options)) { std::cout << " " << e.path() << '\n'; } } } else { std::cout << "path " << p << " does not exist\n"; } return 0; }

Output

output of program using follow symlinks feature with file handling in c++ 17

Namespace std::filesystem

All the above features are available under namespace std::filesystem. Therefore, in all the examples above, each command prefixes “std::filesystem”. However, for writing a cleaner code and avoiding the prefix in all items, the following 2 methods are used:

  1. Explicitly include the namespace with using directive, Example,
using namespace std::filesystem;

2. Define a namespace variable

namespace fs = std::filesystem;

Therefore, use this fs as in following snippet

if (fs::exists(p)) { if (fs::is_directory(p)) { } }

Complete Example Again – cleaner code

The above example is now including namespace with using directive. Therefore, the result is that same code looks much cleaner now.

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathDir = "mydir"; path p {pathDir}; if (exists(p)) { if (is_directory(p)) { auto options { directory_options::follow_directory_symlink }; cout << p << " is a directory:\n"; for (auto& e : recursive_directory_iterator (p, options)) { std::cout << " " << e.path() << '\n'; } } } else { std::cout << "path " << p << " does not exist\n"; } return 0; }

Getting file type with Alternate method : filesystem::status

The above examples had called specific functions to know the type of file.

  • is_regular_file(std::filesystem::path&)
  • is_directory(std::filesystem::path&)
  • is_symlink(std::filesystem::path&)
  • is_socket(std::filesystem::path&)
  • is_character_file(std::filesystem::path&)
  • is_fifo(std::filesystem::path&)
  • etc.

However, there is alternative way to achieve the same result. This comes with the function std::status(std::filesystem::path&). The function status( ) returns an object of type “file_status”. This file_status has a type( ) member, which shall return an enum result denoting the file type. Ultimately, a user can use a switch statement to check the type.

enum class file_type { none = /* unspecified */, not_found = /* unspecified */, regular = /* unspecified */, directory = /* unspecified */, symlink = /* unspecified */, block = /* unspecified */, character = /* unspecified */, fifo = /* unspecified */, socket = /* unspecified */, unknown = /* unspecified */, /* implementation-defined */ };

Example using file_type enum

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathfile = "mydir"; path p {pathfile}; if (exists(p)) { switch(status(p).type()) { case file_type::regular: { cout << p << " is a file:\n"; break; } case file_type::directory: { cout << p << " is a directory:\n"; break; } default: { cout << p << " is a something else\n"; } } } else { std::cout << "path " << p << " does not exist\n"; } return 0; }

Writing a file in C++

C++ uses fstream classes to write and read from a file. This feature is available from standard C++ and not specific to C++17.

Following simple example demonstrates a file write.

#include <iostream> //main header #include <fstream> using namespace std; //for namespace int main() { std::string pathfile = "myfile"; std::ofstream p{pathfile}; if (!p) { std::cerr << "Error opening for write" << endl; std::exit(EXIT_FAILURE); } p << "The testing starts" << endl; p << "This is a file created for testing"; p << endl; p << "The testing ends" << endl; return 0; }

Corresponding example for reading the file

#include <iostream> //main header #include <fstream> using namespace std; //for namespace int main() { std::string pathfile = "myfile"; std::ifstream p{pathfile}; if (!p) { std::cerr << "Error opening for reading" << endl; std::exit(EXIT_FAILURE); // exit program with failure } string s; while(getline(p, s)) // Discards newline char cout << s << "\n"; return 0; }

Creating Directory(s) in C++17

There are 2 functions available for creation of directory

  • create_directory( )
  • create_directories( )

As the name suggests, the former creates a specific directory inside a hierarchy. However, latter creates complete directory tree. Actually, this one is equivalent to “mkdir” with “-p” option.

When the target already exists (directory of same name) the functions just returns “false”. However, in case of any other error, they throw exception.

Simple example of create_directory( )

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathdir1 = "mydir"; if(!create_directory(pathdir1)) { cout << "directory already exists!" << endl; return 0; } cout << pathdir1 << " created successfully!" << endl; return 0; }

Simple example of create_directories( )

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathdir1 = "mydir1/mydir2/mydir3/mydir4"; if(!create_directories(pathdir1)) { cout << "directory already exists!" << endl; return 0; } cout << pathdir1 << " created successfully!" << endl; return 0; }

Create a link in C++17

There are 2 functions for creating symbolic links.

  • create_symlinks( )
  • create_directory_symlinks( )

The names are suggesting the background in both cases. A program should always use former for symlinks to files. Secondly, for directories, the later cousin is suitable. Although, create_symlinks( ) may also work with directories in some cases, however, the concept is operating system dependent. Example, some OS do not support symlinks for directories at all whereas some OS need special handling for directory symlinks. Therefore, for ensuring portability, the proper versions should be used.

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { create_symlink("myfile", "link1"); create_directory_symlink("mydir1", "link2"); return 0; }

Exception for file handling using filesystem library

File operations are generally very complicated. This is because, it depends on many external factors. Hence, It is possible that filesystem library do not handle all error cases with proper return codes. Therefore, the filesystem may throw an exception for such cases.

try { } catch (std::filesystem::filesystem_error& e) { }

Possible exception scenarios:

  • create_directory can fail. Because, a file or link (not directory) of same name may already exist
  • create_symlinks can fail due to wrong inputs
  • OS errors
  • etc.

Basic example for exception with create_directory( )

In the following example, the directory hierarchy provided as argument actually do not exist. The call shall throw exception.

#include <iostream> //main header #include <filesystem>//for filesystem using namespace std; //for namespace using namespace std::filesystem; int main() { std::string pathdir1 = "mydir/dir1/dir2/dir3"; //dir2 does not exist" try { if(!create_directory(pathdir1)) { cout << "directory already exists!" << endl; return 0; } cout << pathdir1 << " created successfully!" << endl; } catch (filesystem_error& e) { std::cerr << "EXCEPTION: " << e.what() << '\n'; std::cerr << " path1: \"" << e.path1().string() << "\"\n"; } return 0; }
output of code using exception with file handling in c++ 17

File Time (file_time_type) Utility

Getting Last Write Time of a File

The filesystem has a utility function last_write_time(path) which returns last modification time or update time of file. The return type is a special type of template class “time_point” in chrono library. It needs to be noted that the return value here is not a system clock time. The file_time_type value needs to convert to system time, i.e. to class type “time_t”. This is because, the chrono utility supports time value of multiple resolutions. The time returned by chrono needs proper conversion to system clock to print the actual time.

namespace std::filesystem { using file_time_type = chrono::time_point<trivialClock>; }

The following code demonstrates how to convert time from filesystem to std.

filesystem::file_time_type => std::time_t

#include <iostream> //main header #include <filesystem>//for filesystem #include <chrono> //for chrono util using namespace std; int main() { filesystem::path p = "myfile"; filesystem::file_time_type lwt = last_write_time(p); std::time_t t = chrono::system_clock::to_time_t( chrono::system_clock::now() - (filesystem::file_time_type::clock::now() - lwt) ); string ts = ctime(&t); cout << "last update of " << p << ": " << ts << '\n'; return 0; }

Output

printing the last update time of a file using filesystem

Main Funda: File handling in C++ 17 provides a lot of features. These have come mainly from boost filesystem library.

Related Topics:

Class Template Argument Deduction in C++17
What is a Tuple, a Pair and a Tie in C++
C++ Multithreading: Understanding Threads
What is Copy Elision, RVO & NRVO?
Lambda in C++11
Lambda in C++17
std::chrono in C++ 11
Thread Synchronization with Mutex
Template type deduction in functions
How std::forward( ) works?
How std::move() function works?
What is reference collapsing?

Share the Article

Leave a Reply

Your email address will not be published. Required fields are marked *