【问题标题】:Why can't I insert this transformed directory_iterator into a vector?为什么我不能将此转换后的 directory_iterator 插入向量中?
【发布时间】:2021-08-05 22:01:45
【问题描述】:

我正在尝试使用其insert(const_iterator pos, InputIt first, InputIt last) 成员函数模板将转换后的目录条目范围插入向量中。 不幸的是,我无法在 GCC 11.1.0 下编译以下代码,根据 https://en.cppreference.com/w/cpp/compiler_support 应该支持范围。

#include <filesystem>
#include <vector>
#include <ranges>
#include <iterator>

namespace fs = std::filesystem;
namespace ranges = std::ranges;
namespace views = std::views;

// no solution
namespace std {
    template <typename F>
    struct iterator_traits<ranges::transform_view<ranges::ref_view<fs::directory_iterator>, F>> {
        using iterator_category = std::input_iterator_tag;
    };
}

int main() {
    std::vector<fs::path> directory_tree;

    auto subdir = fs::directory_iterator(".");
    ranges::input_range auto subdir_names = subdir
        | views::transform([](const auto& entry) { return entry.path(); /* can be more complex*/ })
        | views::common;
    
    // replacing subdir_names with subdir works
    std::input_iterator auto b = ranges::begin(subdir_names);
    std::input_iterator auto e = ranges::end(subdir_names);
    directory_tree.insert(
        directory_tree.begin(),
        b,
        e
    );
}

错误信息主要是说:

error: no matching function for call to 'std::vector<std::filesystem::__cxx11::path>::insert(std::vector<std::filesystem::__cxx11::path>::iterator, std::ranges::transform_view<std::ranges::ref_view<std::filesystem::__cxx11::directory_iterator>, main()::<lambda(const auto:16&)> >::_Iterator<false>&, std::ranges::transform_view<std::ranges::ref_view<std::filesystem::__cxx11::directory_iterator>, main()::<lambda(const auto:16&)> >::_Iterator<false>&)'

再往下:

error: no type named ‘iterator_category’ in ‘struct std::iterator_traits<std::ranges::transform_view<std::ranges::ref_view<std::filesystem::__cxx11::directory_iterator>, main()::<lambda(const auto:15&)> >::_Iterator<false> >’

我尝试将上述特化添加到std::iterator_traits 以获取相关的迭代器类型,但无济于事。我想了解为什么这不能编译,如果可能的话,如何修复它。我想避免创建临时向量。

如果需要更多 gcc 的错误信息,请告诉我。

【问题讨论】:

    标签: c++ stdvector c++20 std-ranges std-filesystem


    【解决方案1】:

    fs::directory_iterator 是一个输入范围。这意味着当你通过transform 调整它时,你仍然会得到一个输入范围(自然)。这个转换后的范围的迭代器有一个后缀operator++,它返回void

    这可以说是 C++98 迭代器模型中的一个缺陷,它仍然要求甚至输入迭代器有一个返回原始类型的后缀 operator++。即使这必然是一个悬空操作。在 C++20 迭代器模型中,postfix-increment 可以为输入迭代器返回void

    因此,您返回的转换后的范围(views::common 是无操作的,它已经是 common一个 C++20 输入范围(正如您正在验证的那样)但它不是任何类型的 C++98/C++17 范围,因为它的迭代器甚至不满足 Cpp17InputIterator 由于那个后缀增量规则 - 所以它的迭代器甚至不用费心提供iterator_category

    那就是:

    directory_tree.insert(directory_tree.begin(), b, e);
    

    失败,因为此函数需要满足 Cpp17InputIterator 的类型,而 be 不满足。


    解决方法是:

    ranges::copy(subdir_names, std::inserter(directory_tree, directory_tree.begin()));
    

    甚至将这两个步骤结合起来:

    ranges::copy(subdir
            | views::transform([](const auto& entry) { return entry.path(); /* can be more complex*/ }),
            std::inserter(directory_tree, directory_tree.begin())
    );
    

    在这里,我们只要求源代码范围是 C++20 input_range(就是这样)。

    目的是很快你就可以写了:

    directory_tree.insert_range(
            directory_tree.begin(),
            subdir
            | views::transform([](const auto& entry) { return entry.path(); }));
    

    但这要等到 C++23。

    【讨论】:

    • 非常感谢您的解释 :) 我是否正确理解 ranges::copy(subdir_names, std::inserter(directory_tree, directory_tree.begin())); 具有复杂性 O(size(directory_tree) * distance(pos, end(directory_tree))) 而 directory_tree.insert(directory_tree.begin(), b, e); 将是O(size(directory_tree) + distance(pos, end(directory_tree))),因为insert(iter, value) 只是调用vector::insert(iter, value)?假设 directory_tree 已经有条目。
    • @neop 为什么它们会有不同的复杂性?
    • 根据 cppreference vector::insert(const_iterator pos, InputIt first, InputIt last) 是“在 std::distance(first, last) 中线性加上 pos 和容器末端之间的距离线性”。 ranges::copy 是“确切(最后 - 第一个)分配”,其中分配给 std::insert_iterator 调用 vector::insert(const_iterator pos, const T&amp; value) 这是“在 pos 和容器末端之间的距离上是线性的”。至少这是我的理解。范围插入可能会为整个(甚至是输入)范围腾出空间,而副本会为每个插入的元素移动每个元素。
    • @neop 由于您正在处理输入迭代器,因此无法在内部进行单个分配,所以它是相同的。您可能希望在末尾而不是开头插入(如果您真的希望它们在前面结束,那就是 rotate)。
    • 如果我理解正确,我不同意。推动整个范围然后旋转到正确位置会比复制(范围,插入器)具有更好的复杂性,因为范围仅旋转一次,而不是在每个插入时旋转一次。顺便提一句。哇,轮换无处不在。但我同意无法预先知道分配大小。就我而言,我实际上想插入中间,但我认为它与问题无关。
    【解决方案2】:

    只是想添加一个替代解决方法。有关问题的解释,请参阅Barry's answer。下面的适配器可用于将迭代器适配到vector::insert的接口。

    #include <filesystem>
    #include <vector>
    #include <ranges>
    #include <iterator>
    
    namespace fs = std::filesystem;
    namespace ranges = std::ranges;
    namespace views = std::views;
    
    template <typename It>
    struct insertable_iterator_adapter : It {
        using iterator_category = std::input_iterator_tag;
        using difference_type = std::ptrdiff_t;
        using value_type = std::decay_t<decltype(*std::declval<It>())>;
        using pointer = value_type*;
        using reference = value_type&;
    };
    
    int main() {
        std::vector<fs::path> directory_tree;
    
        auto subdir = fs::directory_iterator(".");
        ranges::input_range auto subdir_names = subdir
            | views::transform([](const auto& entry) { return entry.path(); });
    
        directory_tree.insert(
            directory_tree.begin(),
            insertable_iterator_adapter{ranges::begin(subdir_names)},
            insertable_iterator_adapter{ranges::end(subdir_names)}
        );
    }
    

    虽然我不确定该解决方案适用于多少其他算法。


    编辑: 我不认为使用std::inserter

    ranges::copy(subdir_names, std::inserter(directory_tree, directory_tree.begin()));
    

    当 directory_tree 已经有值时是一个好主意,因为复杂度是 O(n * k),其中 n = previous directory_tree.size() 和 k = subdir_names.size()

    根据 cppreference vector::insert(const_iterator pos, InputIt first, InputIt last) 是“线性 std::distance(first, last) 加上线性 pos 和容器末端之间的距离”。 range::copy 是“完全(最后 - 第一个)赋值”,其中对 std::insert_iterator 的赋值调用 vector::insert(const_iterator pos, const T& value) ,它是“在 pos 和容器末端之间的距离上是线性的” 这是因为当一次插入一个元素时,向量每次都需要将所有内容都移动一个。

    另一方面,vector::insert(const_iterator pos, InputIt first, InputIt last) 是“std::distance(first, last) 中的线性加上 pos 和容器末端之间的距离的线性”或 O( k + n)。

    具有相同 O(k + n) 复杂度的另一种选择是

    ranges::copy(subdir_names, std::back_inserter(directory_tree));
    std::rotate(
      ranges::begin(directory_tree),
      ranges::begin(directory_tree) + previous_size,
      ranges::end(directory_tree)
    );
    

    如果您的向量之前为空,则三个选项之间没有复杂性差异,但也没有理由插入而不是 push_back。

    Benchmark 强调复杂性问题。

    对我来说,带有适配器的选项是最清晰的,并且具有很好的复杂性。如果您知道如何为 Cpp17InputIterator 编写更通用(更好)的适配器,请告诉我。

    【讨论】:

    • 是的,不要这样做。
    • 您能详细说明一下吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-22
    • 1970-01-01
    相关资源
    最近更新 更多