c++templateslsstd-filesystemstd-variant

Use of `std::variant` to select types after command prompt input, is that even possible?


I am at a very early stage in writing a C++ program replicating the most basic functions of the ls bash command.

I use the <filesystem> header.

The std::filesystem::directory_iterator and std::filesystem::recursive_directory_iterator from that library help me traverse the input file path in a shallow or recursive manner, respectively.

The input receives an argv[1], which should be composed of an initial hyphen -, followed by letters standing for option flags. Flag R stands for recursive traversal of all subdirectories, otherwise, look only into the given path.

argv[2] is the file path to explore.

My question however is about how to run this in a templated manner, or if it is even at all possible.

As a test, I am using std::variant in a function option_R() which selects the correct directory_iterator depending on whether the -R flag is present on input or not.

Since this would be runtime polymorphism, I am skeptical I can implement something like the below:

#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <variant>

#include <type_traits>
#include <concepts>

using namespace std;
using namespace std::filesystem;

template<typename T> concept isIter = 
  is_same<T, directory_iterator>::value || is_same<T, recursive_directory_iterator>::value;

template <isIter dirIterator>
dirIterator option_R(std::string_view options, const path &my_path) {
  variant<directory_iterator, recursive_directory_iterator> my_iter;
  if (options.find('R') != string::npos) {
    my_iter.emplace<recursive_directory_iterator>(my_path, directory_options::skip_permission_denied);
    return get<recursive_directory_iterator>(my_iter);
  } else {
    my_iter.emplace<directory_iterator>(my_path);
    return get<directory_iterator>(my_iter);
  }
}

template <isIter dirIterator>
void traverse(isIter &my_iter, vector<string> &my_vec) {
  cout << "This function traverses the given dir path, "
       << "stores all filenames found inside a vector." << endl;
  for (const auto &entry : my_iter)
    my_vec.push_back(entry.path().string());
}

int main(int argc, const char **argv) {
  const path sys_path{argv[2]};
  const string options{argv[1]};

  vector<string> files;
  if (options[0] == '-') {
    if (is_directory(sys_path)) {
      isIter file_iterator = option_R(options, sys_path);
      traverse(file_iterator, files);
    }
    cout << "after this the program will parse other options" << endl;
  } else {
    cout << "simply print all files" << endl;
  }
}

The above does not compile, among other reasons because I leisurely use the concept isIter to invoke option_R() in main.

Would there be a way to write option_R(), so that it just does one thing: select the correct directory_iterator according to whether flag -R is given or not - and pass it back to main()?

Would be easy to pack option_R() and traverse() together in only one function, but then this would do multiple things, and the code would be more difficult to understand.


Solution

  • You can achieve what you are trying to do with std::variant and std::visit.

    A std::variant is a safer union, which you can use to store both a std::filesystem::directory_iterator and a std::filesystem::recursive_directory_iterator, without having the need to allocate memory for both iterators separately.

    With std::visit you can then have your std::variant passed to a templated (or overloaded) function which can work with all types the std::variant has.

    Using this, we can have your option_R() function return a std::variant of both iterators mentioned above and then the traverse() function takes one such std::variant as argument and then uses it with std::visit to iterate over all files in the expected way.

    #include <iostream>
    #include <vector>
    #include <string>
    #include <filesystem>
    #include <variant>
    
    using IterVariant = std::variant<std::filesystem::directory_iterator, std::filesystem::recursive_directory_iterator>;
    
    IterVariant option_R(std::string_view options, const std::filesystem::path& my_path) {
        if (options.find('R') != std::string_view::npos) {
            return std::filesystem::recursive_directory_iterator(my_path, std::filesystem::directory_options::skip_permission_denied);
        }
        return std::filesystem::directory_iterator(my_path);
    }
    
    void traverse(IterVariant& my_iter, std::vector<std::string>& my_vec) 
    {
        std::cout << "This function traverses the given dir path, "
            << "stores all filenames found inside a vector.\n";
    
        std::visit([&my_vec](auto&& it) {
            for (const auto& entry : it) {
                my_vec.push_back(entry.path().string());
            }
        }, my_iter);
    }
    
    int main(int argc, const char** argv) {
        if (argc < 3) {
            return 1;
        }
        const std::filesystem::path sys_path{ argv[2] };
        const std::string options{ argv[1] };
    
        std::vector<std::string> files;
        if (options[0]=='-') {
            if (std::filesystem::is_directory(sys_path)) {
                IterVariant file_iterator = option_R(options, sys_path);
                traverse(file_iterator, files);
            }
            std::cout << "after this the program will parse other options\n";
        } else {
            std::cout << "simply print all files\n";
        }
    
        for (std::string& file_name : files) {
            std::cout << file_name << '\n';
        }
    
        return 0;
    } 
    

    (live example)