【问题标题】:(Re)Using std::algorithms with non-standard containers(重新)在非标准容器中使用 std::algorithms
【发布时间】:2013-06-01 15:18:14
【问题描述】:

我有一个“列”容器类型:

struct MyColumnType { 
  // Data: Each row represents a member of an object.
  vector<double> a;   // All vectors are guaranteed to have always
  vector<string> b;   // the same length.
  vector<int> c;

  void copy(int from_pos, int to_pos); // The column type provides an interface
  void swap(int pos_a, int pos_b);     // for copying, swapping, ...

  void push_back();      // And for resizing the container.
  void pop_back();
  void insert(int pos);
  void remove(int pos);
  // The interface can be extended/modified if required
};

用法:

// If table is a constructed container with elements stored 
// To acces the members of the object stored at the 4th position:
table.a[4] = 4.0;
table.b[4] = "4th";
table.c[4] = 4;

问题:如何为这种容器创建符合标准的随机访问迭代器(可能还需要代理引用类型)?

我希望能够将std::algorithms 用于我的类型的随机访问迭代器,例如sort(注意:对于排序比较将由用户定义的函子提供,例如 lambda)。

特别是迭代器应该提供类似于

的接口
struct {
  double& a;
  string& b;
  int& c;
};

注意 0: 允许使用 C++11/C++14。

注意 1: 有一篇旧论文 http://hci.iwr.uni-heidelberg.de/vigra/documents/DataAccessors.ps 进行了类似的尝试。但是,我无法让他们的方法与 sort 一起使用。使用代理类型方法很难满足像 defaultConstructible 这样的要求(为什么std::sort 要求类型是默认可构造的而不是可交换的,这超出了我的理解)。

注意 2: 我不能执行以下操作:

struct MyType {
  double a;
  string b;
  int c;
};

std::vector<MyType> v;

然后使用std::algorithm

动机:表现。一个高速缓存行通常是 64 字节,即 8 个双精度。在这个简单的结构中,如果你遍历双精度数,你正在用一个字符串和一个 int 污染一个缓存行。在其他情况下,每个高速缓存行可能只获得 1 个双重传输。也就是说,您最终会使用可用内存带宽的 1/8。如果您需要迭代几个 Gb 的双精度数据,这个简单的决定可以将您的应用程序性能提高 6-7 倍。不,我不能放弃。

奖励:答案应尽可能笼统。将向容器类型添加/删除字段视为向结构添加/删除成员。您不希望每次添加新成员时都更改大量代码。

【问题讨论】:

  • @jogojapan 在那个问题中,他想按顺序检查他的成员(更像是容器的加入/链接)。这里更像是一个 zip 操作。我想按顺序遍历成员,但同时。
  • 是的,但是那里列出了各种使创建新迭代器类型更容易的工具。它们会很有用。
  • @jogojapan 我真的不认为 boost 迭代器外观是为了解决这个问题而设计的。无论如何,所需的hackery 数量可能会迫使您手动定义所有迭代器成员函数和特征。
  • 您对MyColumnType / 这个容器的一个元素的比较函数(例如&lt;)是如何定义的?您需要对std::sort 进行严格的总排序。

标签: c++ algorithm proxy iterator


【解决方案1】:

我认为这样的事情可能符合标准。它使用一些 C++11 特性来简化语法,但也可以更改为符合 C++03 AFAIK。

经过测试并与 clang++3.2 配合使用

前奏:

#include <vector>
#include <string>
#include <utility>  // for std::swap
#include <iterator>

using std::vector;
using std::string;


// didn't want to insert all those types as nested classes of MyColumnType
namespace MyColumnType_iterator
{
    struct all_copy;
    struct all_reference;
    struct all_iterator;
}


// just provided `begin` and `end` member functions
struct MyColumnType {
    // Data: Each row represents a member of an object.
    vector<double> a;   // All vectors are guaranteed to have always
    vector<string> b;   // the same length.
    vector<int> c;

    void copy(int from_pos, int to_pos); // The column type provides an itface
    void swap(int pos_a, int pos_b);     // for copying, swapping, ...

    void push_back();      // And for resizing the container.
    void pop_back();
    void insert(int pos);
    void remove(int pos);
    // The interface can be extended/modified if required


    using iterator = MyColumnType_iterator::all_iterator;
    iterator begin();
    iterator end();
};

迭代器类:value_type (all_copy)、reference 类型 (all_reference) 和迭代器类型 (all_iterator)。迭代是通过保持和更新三个迭代器(每个 vector 一个)来完成的。不过,我不知道这是否是性能最高的选项。

它是如何工作的:std::iterator_traits 为迭代器定义了几个相关的类型: [iterator.traits]/1

iterator_traits&lt;Iterator&gt;::difference_type
iterator_traits&lt;Iterator&gt;::value_type
iterator_traits&lt;Iterator&gt;::iterator_category
分别定义为迭代器的差分类型、值类型和迭代器类别。另外,类型
iterator_traits&lt;Iterator&gt;::reference
iterator_traits&lt;Iterator&gt;::pointer
应定义为迭代器的引用和指针类型,即对于一个迭代器对象a,分别与*aa-&gt;的类型相同

因此,您可以引入一个结构 (all_reference),将三个引用保持为 reference 类型。这个类型是*a的返回值,其中a是迭代器类型(可能是const-qualified)。需要有一个不同的value_type,因为某些标准库算法(例如sort)可能希望创建一个局部变量来临时存储*a 的值(通过复制或移动到局部变量中)。在这种情况下,all_copy 提供了此功能。

您不需要在自己的循环中使用它 (all_copy),因为它可能会影响性能。

namespace MyColumnType_iterator
{
    struct all_copy;

    struct all_reference
    {
        double& a;
        string& b;
        int& c;

        all_reference() = delete;
        // not required for std::sort, but stream output is simpler to write
        // with this
        all_reference(all_reference const&) = default;
        all_reference(double& pa, string& pb, int& pc)
            : a{pa}
            , b{pb}
            , c{pc}
        {}

        // MoveConstructible required for std::sort
        all_reference(all_reference&& other) = default;
        // MoveAssignable required for std::sort
        all_reference& operator= (all_reference&& other)
        {
            a = std::move(other.a);
            b = std::move(other.b);
            c = std::move(other.c);

            return *this;
        }

        // swappable required for std::sort
        friend void swap(all_reference p0, all_reference p1)
        {
            std::swap(p0.a, p1.a);
            std::swap(p0.b, p1.b);
            std::swap(p0.c, p1.c);
        }

        all_reference& operator= (all_copy const& p) = default;
        all_reference& operator= (all_copy&& p) = default;

        // strict total ordering required for std::sort
        friend bool operator< (all_reference const& lhs,
                               all_reference const& rhs);
        friend bool operator< (all_reference const& lhs, all_copy const& rhs);
        friend bool operator< (all_copy const& lhs, all_reference const& rhs);
    };

    struct all_copy
    {
        double a;
        string b;
        int c;

        all_copy(all_reference const& p)
            : a{p.a}
            , b{p.b}
            , c{p.c}
        {}
        all_copy(all_reference&& p)
            : a{ std::move(p.a) }
            , b{ std::move(p.b) }
            , c{ std::move(p.c) }
        {}
    };

std::sort 需要有一个比较函数。出于某种原因,我们必须提供所有三个。

    bool operator< (all_reference const& lhs, all_reference const& rhs)
    {
        return lhs.c < rhs.c;
    }
    bool operator< (all_reference const& lhs, all_copy const& rhs)
    {
        return lhs.c < rhs.c;
    }
    bool operator< (all_copy const& lhs, all_reference const& rhs)
    {
        return lhs.c < rhs.c;
    }

现在,迭代器类:

    struct all_iterator
        : public std::iterator < std::random_access_iterator_tag, all_copy >
    {
        //+ specific to implementation
        private:
            using ItA = std::vector<double>::iterator;
            using ItB = std::vector<std::string>::iterator;
            using ItC = std::vector<int>::iterator;
            ItA iA;
            ItB iB;
            ItC iC;

        public:
            all_iterator(ItA a, ItB b, ItC c)
                : iA(a)
                , iB(b)
                , iC(c)
            {}
        //- specific to implementation


        //+ for iterator_traits
            using reference = all_reference;
            using pointer = all_reference;
        //- for iterator_traits


        //+ iterator requirement [iterator.iterators]/1
            all_iterator(all_iterator const&) = default;            // CopyConstructible
            all_iterator& operator=(all_iterator const&) = default; // CopyAssignable
            ~all_iterator() = default;                              // Destructible

            void swap(all_iterator& other)                          // lvalues are swappable
            {
                std::swap(iA, other.iA);
                std::swap(iB, other.iB);
                std::swap(iC, other.iC);
            }
        //- iterator requirements [iterator.iterators]/1
        //+ iterator requirement [iterator.iterators]/2
            all_reference operator*()
            {
                return {*iA, *iB, *iC};
            }
            all_iterator& operator++()
            {
                ++iA;
                ++iB;
                ++iC;
                return *this;
            }
        //- iterator requirement [iterator.iterators]/2

        //+ input iterator requirements [input.iterators]/1
            bool operator==(all_iterator const& other) const        // EqualityComparable
            {
                return iA == other.iA;  // should be sufficient (?)
            }
        //- input iterator requirements [input.iterators]/1
        //+ input iterator requirements [input.iterators]/2
            bool operator!=(all_iterator const& other) const        // "UnEqualityComparable"
            {
                return iA != other.iA;  // should be sufficient (?)
            }

            all_reference const operator*() const                   // *a
            {
                return {*iA, *iB, *iC};
            }

            all_reference operator->()                              // a->m
            {
                return {*iA, *iB, *iC};
            }
            all_reference const operator->() const                  // a->m
            {
                return {*iA, *iB, *iC};
            }

            // ++r already satisfied

            all_iterator operator++(int)                            // *++r
            {
                all_iterator temp(*this);
                ++(*this);
                return temp;
            }
        //- input iterator requirements [input.iterators]/2

        //+ output iterator requirements [output.iterators]/1
            // *r = o already satisfied
            // ++r already satisfied
            // r++ already satisfied
            // *r++ = o already satisfied
        //- output iterator requirements [output.iterators]/1

        //+ forward iterator requirements [forward.iterators]/1
            all_iterator() = default;                               // DefaultConstructible
            // r++ already satisfied
            // *r++ already satisfied
            // multi-pass must be guaranteed
        //- forward iterator requirements [forward.iterators]/1

        //+ bidirectional iterator requirements [bidirectional.iterators]/1
            all_iterator& operator--()                              // --r
            {
                --iA;
                --iB;
                --iC;
                return *this;
            }
            all_iterator operator--(int)                            // r--
            {
                all_iterator temp(*this);
                --(*this);
                return temp;
            }
            // *r-- already satisfied
        //- bidirectional iterator requirements [bidirectional.iterators]/1

        //+ random access iterator requirements [random.access.iterators]/1
            all_iterator& operator+=(difference_type p)             // r += n
            {
                iA += p;
                iB += p;
                iC += p;
                return *this;
            }
            all_iterator operator+(difference_type p) const         // a + n
            {
                all_iterator temp(*this);
                temp += p;
                return temp;
            }
            // doesn't have to be a friend function, but this way,
            // we can define it here
            friend all_iterator operator+(difference_type p,
                                         all_iterator temp)         // n + a
            {
                temp += p;
                return temp;
            }

            all_iterator& operator-=(difference_type p)             // r -= n
            {
                iA -= p;
                iB -= p;
                iC -= p;
                return *this;
            }
            all_iterator operator-(difference_type p) const         // a - n
            {
                all_iterator temp(*this);
                temp -= p;
                return temp;
            }

            difference_type operator-(all_iterator const& p)        // b - a
            {
                return iA - p.iA;   // should be sufficient (?)
            }

            all_reference operator[](difference_type p)             // a[n]
            {
                return *(*this + p);
            }
            all_reference const operator[](difference_type p) const // a[n]
            {
                return *(*this + p);
            }

            bool operator<(all_iterator const& p) const             // a < b
            {
                return iA < p.iA;   // should be sufficient (?)
            }
            bool operator>(all_iterator const& p) const             // a > b
            {
                return iA > p.iA;   // should be sufficient (?)
            }
            bool operator>=(all_iterator const& p) const            // a >= b
            {
                return iA >= p.iA;  // should be sufficient (?)
            }
            bool operator<=(all_iterator const& p) const            // a >= b
            {
                return iA <= p.iA;  // should be sufficient (?)
            }
        //- random access iterator requirements [random.access.iterators]/1
    };
}//- namespace MyColumnType_iterator


MyColumnType::iterator MyColumnType::begin()
{
    return { a.begin(), b.begin(), c.begin() };
}
MyColumnType::iterator MyColumnType::end()
{
    return { a.end(), b.end(), c.end() };
}

使用示例:

#include <iostream>
#include <cstddef>
#include <algorithm>


namespace MyColumnType_iterator
{
    template < typename char_type, typename char_traits >
    std::basic_ostream < char_type, char_traits >&
    operator<< (std::basic_ostream < char_type, char_traits >& o,
                std::iterator_traits<MyColumnType::iterator>::reference p)
    {
        return o << p.a << ";" << p.b << ";" << p.c;
    }
}

int main()
{
    using std::cout;

    MyColumnType mct =
    {
          {1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1}
        , {"j", "i", "h", "g", "f", "e", "d", "c", "b", "a"}
        , {10,    9,   8,   7,   6,   5,   4,   3,   2,   1}
    };

    using ref = std::iterator_traits<MyColumnType::iterator>::reference;
    std::copy(mct.begin(), mct.end(), std::ostream_iterator<ref>(cout, ", "));
    std::cout << std::endl;

    std::sort(mct.begin(), mct.end());
    std::copy(mct.begin(), mct.end(), std::ostream_iterator<ref>(cout, ", "));
    std::cout << std::endl;
}

输出:

1;j;10, 0.9;i;9, 0.8;h;8, 0.7;g;7, 0.6;f;6, 0.5;e;5, 0.4;d;4, 0.3;c;3 , 0.2;b;2, 0.1;a;1,
0.1;a;1, 0.2;b;2, 0.3;c;3, 0.4;d;4, 0.5;e;5, 0.6;f;6, 0.7;g;7, 0.8;h;8, 0.9; i;9, 1;j;10,

【讨论】:

  • 为了简化您的设计,我最终得到了几乎相同的东西。 :( 我不认为它比你拥有的更简单。coliru.stacked-crooked.com/…
  • @MooingDuck 是的,我也认为指针+int 作为迭代器,但我记得一些编译器有时会在循环中优化它(pointer[int] 可能比*pointer; ++pointer 慢,因为围绕它进行了优化代码)。例如。在 MSVC10 或 boost 矩阵上使用简单的向量。这可能是快速增量与快速取消引用的权衡。
  • @DyP 我喜欢你的回答。我明天试试。有两件事让我担心。首先,如果我向我的列类型添加一个新字段,我必须更改很多 代码。我会考虑让它更通用。然后是所有这些比较功能。您是否通过传递用户定义的比较来测试它,例如通过 lambda?定义所有这些比较函数似乎很奇怪(我也不知道为什么需要它们)。如果 C++11 lambda 不起作用,C++14 lambda 可能会起作用。我明天也要测试一下。最后,感谢这么好的回答!我真的很感激!
  • @gnzlbg 您可以使用元组和一些通用编程来简化更改列类型的字段,是的。 Mooing Duck 仅使用一个迭代器的方法在更改这些字段时也少了很多麻烦。比较函数确实很奇怪。它们是必需的,因为 std::sort 必须比较 *a &lt; *b,但我不知道只提供一个(依赖于隐式转换)有什么问题。
  • @DyP:是的,我的迭代器可能会慢一些,但它们更小,推进更快。我避免查看您的代码以尝试查看我是否想出了一些不同的东西,并且还没有比较它们。有趣的是,您有三个 operator&lt;,隐式转换与我的比较有效。
【解决方案2】:

如果您真的很关心性能并且想要使用 std::sort 对容器进行排序,请使用允许您提供自定义比较对象的重载:

template <class RandomAccessIterator, class Compare>
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

.. 并将索引数组排序到容器中。方法如下:

您的容器中需要以下成员:

struct MyColumnType { 
    ...

    int size() const;

    // swaps columns
    void swap(int l, int r);

    // returns true if column l is less than column r
    bool less(int l, int r) const;

    ...
};

然后定义如下比较对象:

struct MyColumnTypeLess
{
    const MyColumnType* container;
    MyColumnTypeLess(const MyColumnType* container)
        : container(container)
    {
    }
    bool operator()(int l, int r) const
    {
        return container->less(l, r);
    }
};

并使用它对索引数组进行排序:

void sortMyColumnType(MyColumnType& container)
{
    std::vector<int> indices;
    indices.reserve(container.size());
    // fill with [0, n)
    for(int i = 0; i != container.size(); ++i)
    {
        indices.push_back(i);
    }
    // sort the indices
    std::sort(indices.begin(), indices.end(), MyColumnTypeLess(&container));
}

容器的“less”成员控制排序的顺序:

bool MyColumnType::less(int l, int r) const
{
    // sort first by a, then b, then c
    return a[l] != a[r] ? a[l] < a[r]
        : b[l] != b[r] ? b[l] < b[r]
        : c[l] < c[r];
}

排序后的索引数组可用于进一步的算法 - 您可以避免复制实际数据,直到需要为止。

所有与 RandomAccessIterator 一起使用的 std 算法都有重载,允许您指定自定义比较对象,因此它们也可以与此技术一起使用。

【讨论】:

  • 这并没有回答如何使用在随机访问迭代器上工作的 all 算法的问题。此外,这需要O(N) 额外的存储空间。就地排序(为std::sort 指定)被定义为使用少于O(N) 额外存储的排序(例如,O(log N) 将被允许)。因此请注意,您的建议允许您使用 std::sort 函数,但如果您将整个容器视为正在排序的东西,而不仅仅是您刚刚创建的临时索引数组,则没有它提供的保证。跨度>
  • 要找到std::sort 与您的容器的实际成本,您必须将它执行的操作数乘以这些操作的成本。根据您的标准库实现,std::sort 最终可能会复制元素 - 复制 std::string 比复制 int 贵一个数量级。如果您像您所说的那样关心性能,那么在具有非常复杂元素的容器上直接使用诸如std::sort 之类的算法是不明智的。
  • 你是对的,复制字符串比复制整数更昂贵。如果您的字符串因 SSO 而变小,复制它们的成本不会高多少。但是,大多数 HPC 应用程序不需要它们,而是需要其他复杂类型(我需要具有良好局部性的 Eigen3 矩阵)。如果您有字符串,我建议您重新考虑您的设计,或者如果您知道最大大小,则将它们替换为堆栈分配的字符串。如果您不知道最大尺寸并且您确实需要它们,那么您将不得不忍受它。但是,它们会导致随机访问内存,从而破坏您的性能。
  • 但是,您对成本的看法并不正确。降低实际成本的最佳方法是拥有可预测的内存访问模式(内存中的高级线性)和完美的空间和时间内存局部性。您的方法具有非常糟糕的时间局部性,并且需要为整数数组提供额外的带宽。在最坏的情况下,您需要将整个容器和一个 int 数组从主内存移动两次到 CPU:第一次置换 int 数组,第二次将这些置换应用于其余元素。排序复制元素彼此靠近(可能在缓存中)-> 很快!
  • 详细说明最坏的情况:第一次需要置换 int 数组。并且比较函数可能需要访问容器的所有元素。第二次排列容器时,您的排列必须访问排列后的整数数组。这两个传球是性能杀手。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-30
  • 2011-06-05
  • 2021-11-03
  • 1970-01-01
  • 2019-12-22
  • 1970-01-01
相关资源
最近更新 更多