I am trying to write a modern c++ wrapper around the c header, dirent.h.
To read the contents of a directory in C, something like the following is written:
int listdir(const char *path) {
struct dirent *entry;
DIR *dp;
dp = opendir(path);
if (dp == NULL) {
perror("opendir");
return -1;
}
while((entry = readdir(dp)))
puts(entry->d_name);
closedir(dp);
return 0;
}
Converting this to modern c++, I have the following (where m_dir and m_dirent are std::unique_ptr
's, and m_files is a std::vector<string>
)
filesystem::Directory::Directory(std::string dir) : m_dir(opendir(dir.c_str()), closedir),
m_dirent(new struct dirent())
{
//If the directory can not be read, throw an error.
if (!m_dir) {
sdl2::SDLFileSystemRead_Failure ex;
throw ex;
}
while (&(*m_dirent = *readdir(&*m_dir)))
{
m_files.emplace_back(std::string(m_dirent->d_name));
}
}
This only half works. When I wrote it, I was oblivious that I was just checking to see if the address of the expression *m_dirent = *readdir(&*m_dir)
existed(which, of course it does!).
According to The Single UNIX ® Specification, Version 2, readdir(DIR*) returns a null pointer if it has read the last file and there are no more files to be read. However, I am uncertain on how to keep setting the dirent pointer to whatever the dir pointer is reading without calling .reset()
on m_dirent
. Doing this just results in garbage data being read, though, since I assume the file pointer is lost when dirent
is destroyed.
How can I convert this
while((entry = readdir(dp)))
puts(entry->d_name);
into modern C++?
I'm not sure if this count's as modern as I posted something like this in UseNet in the previous millennium (here is a refined version). It evolved slightly to become one of the first components which were used to seed Boost in 1998. Thanks to the work of others it got further developed at Boost and eventually turned into the file system library which formed the basis of the File System TS.
However, it all started with a simple idea: how to expose opendir()
, readdir()
, and closedir()
nicely? The somewhat obvious answer is: using an iterator! Here is a simple version and demo:
#include <iostream>
#include <iterator>
#include <algorithm>
#include <string>
#include <stdexcept>
#include <memory>
#include <dirent.h>
class dir_it
: public std::iterator<std::input_iterator_tag, std::string>
{
std::shared_ptr<DIR> dir;
std::string current;
void advance() {
dirent entry;
dirent* result;
if (!readdir_r(dir.get(), &entry, &result) && result) {
this->current = result->d_name;
}
else {
this->dir.reset();
}
}
public:
dir_it(std::string const& path)
: dir(opendir(path.c_str()), [](DIR* dir){ dir && closedir(dir); }) {
if (!dir) {
throw std::runtime_error("failed to open directory '" + path + "'");
}
this->advance();
}
dir_it(): dir() {}
std::string const& operator*() const { return this->current; }
dir_it& operator++() { this->advance(); return *this; }
dir_it operator++(int) {
dir_it rc(*this);
this->operator++();
return rc;
}
bool operator==(dir_it const& other) const {
return bool(this->dir) == bool(other.dir);
}
bool operator!=(dir_it const& other) const {
return !(*this == other);
}
};
int main() {
std::copy(dir_it("."), dir_it(), std::ostream_iterator<std::string>(std::cout, "\n"));
}
Of course, the file system library in Boost and in the File System TS are much more capable than this somewhat naive implementation. If your implementation ships with an implementation of the TS I'd use that. If it doesn't you might want to consider Boost's implementation.
Comma operator to the rescue !
while(entry.reset(readdir(m_dir.get())), entry)
puts(entry->d_name);
Although, as the comments say, this is not a fantastic API for a filesystem. Iterators that transparently adapt an underlying directory would be cool, for example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With