最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Use of `std::variant` to select types after command prompt input, is that even possible? - Stack Overflow

programmeradmin5浏览0评论

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.

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.

Share Improve this question edited yesterday Milan 2,0173 gold badges16 silver badges47 bronze badges asked yesterday GiogreGiogre 1,5429 silver badges19 bronze badges 8
  • You cannot really pick template parameters based on runtime values. That's why your option_R won't work the way you call it – UnholySheep Commented yesterday
  • 2 Depends on what you consider "a way out of this" - your option_R could just return the std::variant but then you need to adjust how you use traverse to pass in the proper template argument – UnholySheep Commented yesterday
  • 1 @UnholySheep if I can't do polymorphism I would rather remove option_R() entirely, and just do an if-else according to presence or absence of flag -R, inside traverse() – Giogre Commented yesterday
  • 2 @Milan I understand that, your specific example is even mentioned in Accelerated C++ which is a book from the year 2000. At least at the time, the issue was left to the programmer sensibility. Koenig chose to append the ampersand to the type because a reference is a type in its own right. It's also true that prepending the ampersand to the variable name stresses the fact that we refer to a reference type linked to that memory address. Like Koenig I find the appending more expressive, and tongue-in-cheek I would also like to encourage you not to fall victim to the tyranny of edge cases. – Giogre Commented yesterday
  • 2 @Milan you also have somehow disabled code highlighting in the code snippet inside my question – Giogre Commented yesterday
 |  Show 3 more comments

2 Answers 2

Reset to default 2

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)

You are very close. The problem is, you can't have your option_R() function return a compile-time template type that is determined at runtime. You need to have it return the std::variant itself, and then the caller can decide what to do with the value. You can use std::visit() to help you with that, for example:

#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 isDirIter = is_same_v<T, directory_iterator> || is_same_v<T, recursive_directory_iterator>;

using DirIterVariant = variant<directory_iterator, recursive_directory_iterator>;

DirIterVariant option_R(std::string_view options, const path &my_path) {
    if (options.find('R') != string::npos) {
        cout << "Will scan subdirectories recursively" << endl;
        return recursive_directory_iterator(my_path, directory_options::skip_permission_denied);
    } else {
        cout << "Will scan directory only" << endl;
        return directory_iterator(my_path);
    }
}

template <isDirIter dirIterator>
void traverse(dirIterator &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)) {
            DirIterVariant file_iterator_v = option_R(options, sys_path);
            visit([&](auto&& file_iterator) {
                traverse(file_iterator, files);
            },
            file_iterator_v);
        }
        cout << "after this the program will parse other options" << endl;
    } else {
        cout << "simply print all files" << endl;
    }
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论