【发布时间】:2016-09-08 14:11:23
【问题描述】:
我有一个属于某个类的项目的大向量。
struct item {
int class_id;
//some other data...
};
同一个class_id可以在向量中出现多次,向量构造一次后按class_id排序。所以同一类的所有元素在向量中都是相邻的。
我稍后必须处理每个班级的项目,即。我更新了同一类的所有项目,但我不修改不同类的任何项目。由于我必须对所有项目执行此操作,并且代码可轻松并行化,因此我想将 Microsoft PPL 与 Concurrency::parallel_for_each() 一起使用。因此,我需要一个迭代器并提出了一个前向迭代器,它返回具有某个 class_id 作为代理对象的所有项目的范围。代理只是一个std::pair,代理是迭代器的值类型。
using item_iterator = std::vector<item>::iterator;
using class_range = std::pair<item_iterator, item_iterator>;
//iterator definition
class per_class_iterator : public std::iterator<std::forward_iterator_tag, class_range> { /* ... */ };
到现在为止,我已经能够循环遍历我的所有课程并像这样更新项目。
std::vector<item> items;
//per_class_* returns a per_class_iterator
std::for_each(items.per_class_begin(), items.per_class_end(),
[](class_range r)
{
//do something for all items in r
std::for_each(r.first, r.second, /* some work */);
});
当用Concurrency::parallel_for_each 替换std::for_each 时,代码崩溃了。调试后我发现问题出在 ppl.h 中 _Parallel_for_each_helper 中的以下代码,第 2772 行。
// Add a batch of work items to this functor's array
for (unsigned int _Index=0; (_Index < _Size) && (_First != _Last); _Index++)
{
_M_element[_M_len++] = &(*_First++);
}
它使用后增量(因此返回一个临时迭代器),取消引用该临时迭代器并获取取消引用项的地址。这仅在通过取消引用临时对象返回的项目存在时才有效,即。基本上,如果它直接指向容器。所以解决这个问题很容易,尽管每个类 std::for_each 工作循环必须替换为 for 循环。
//it := iterator somewhere into the vector of items (item_iterator)
for(const auto cur_class = it->class_id; cur_class == it->class_id; ++it)
{
/* some work */
}
我的问题是,如果按照我的方式返回代理对象是否违反了标准,或者是否假设每个迭代器都取消引用到永久数据中是由 Microsoft 为其库做出的,但没有记录在案。至少我找不到任何关于 parallel_for_each() 的迭代器要求的文档,除非是随机访问或前向迭代器。我见过the question about forward iterators and vector,但由于我的迭代器的引用类型是const value_type&,我仍然认为我的迭代器按照标准是可以的。那么返回代理对象的前向迭代器仍然是有效的前向迭代器吗?或者换一种说法,迭代器的值类型与实际存储在容器中某处的类型不同是否可以?
可编译示例:
#include <vector>
#include <utility>
#include <cassert>
#include <iterator>
#include <memory>
#include <algorithm>
#include <iostream>
#include <ppl.h>
using identifier = int;
struct item
{
identifier class_id;
// other data members
// ...
bool operator<(const item &rhs) const
{
return class_id < rhs.class_id;
}
bool operator==(const item &rhs) const
{
return class_id == rhs.class_id;
}
//inverse operators omitted
};
using container = std::vector<item>;
using item_iterator = typename container::iterator;
using class_range = std::pair<item_iterator, item_iterator>;
class per_class_iterator : public std::iterator<std::forward_iterator_tag, class_range>
{
public:
per_class_iterator() = default;
per_class_iterator(const per_class_iterator&) = default;
per_class_iterator& operator=(const per_class_iterator&) = default;
explicit per_class_iterator(container &data) :
data_(std::addressof(data)),
class_(equal_range(data_->front())), //this would crash for an empty container. assume it's not.
next_(class_.second)
{
assert(!data_->empty()); //a little late here
assert(std::is_sorted(std::cbegin(*data_), std::cend(*data_)));
}
reference operator*()
{
//if data_ is unset the iterator is an end iterator. dereferencing end iterators is bad.
assert(data_ != nullptr);
return class_;
}
per_class_iterator& operator++()
{
assert(data_ != nullptr);
//if we are at the end of our data
if(next_ == data_->end())
{
//reset the data pointer, ie. make iterator an end iterator
data_ = nullptr;
}
else
{
//set to the class of the next element
class_ = equal_range(*next_);
//and update the next_ iterator
next_ = class_.second;
}
return *this;
}
per_class_iterator operator++(int)
{
per_class_iterator tmp{*this};
++(*this);
return tmp;
}
bool operator!=(const per_class_iterator &rhs) const noexcept
{
return (data_ != rhs.data_) ||
(data_ != nullptr && rhs.data_ != nullptr && next_ != rhs.next_);
}
bool operator==(const per_class_iterator &rhs) const noexcept
{
return !(*this != rhs);
}
private:
class_range equal_range(const item &i) const
{
return std::equal_range(data_->begin(), data_->end(), i);
}
container* data_ = nullptr;
class_range class_;
item_iterator next_;
};
per_class_iterator per_class_begin(container &c)
{
return per_class_iterator{c};
}
per_class_iterator per_class_end()
{
return per_class_iterator{};
}
int main()
{
std::vector<item> items;
items.push_back({1});
items.push_back({1});
items.push_back({3});
items.push_back({3});
items.push_back({3});
items.push_back({5});
//items are already sorted
//#define USE_PPL
#ifdef USE_PPL
Concurrency::parallel_for_each(per_class_begin(items), per_class_end(),
#else
std::for_each(per_class_begin(items), per_class_end(),
#endif
[](class_range r)
{
//this loop *cannot* be parallelized trivially
std::for_each(r.first, r.second,
[](item &i)
{
//update item (by evaluating all other items of the same class) ...
//building big temporary data structure for all items of same class ...
//i.processed = true;
std::cout << "item: " << i.class_id << '\n';
});
});
return 0;
}
【问题讨论】:
-
什么是
items?是std::vector<class_range>吗? -
乍一看,我认为返回代理的迭代器是可以的,但对
parallel_for_each会使用直接指针感到有些不安。但是 cppreference.com 将此列为前向迭代器的条件“如果 a 和 b 比较相等(a == b 在上下文中可转换为真),那么它们要么都是不可取消引用的,要么 *a 和 *b 是绑定到相同的引用对象”这将是一个问题,因为两个代理(即使它们看起来和行为相同)不会是同一个对象。虽然我不知道标准的措辞有多严格(没有看过)。 -
为什么不用
using per_class_iterator = std::vector<class_range>::iterator而不是class per_class_iterator : public std::iterator<std::forward_iterator_tag, class_range> { /* ... */ };? -
或者以class_id为key的
std::multimap怎么样? -
代理迭代器一直存在问题——也许更新的 +14 或 +17 版本的语言使它们没有(或更少)问题。但是,例如,请参阅这篇文章:To Be or Not to Be (an Iterator), by Eric Niebler 以获得对问题的解释 - 其中包括讨论为什么“迭代器的要求”与您认为的不完全一样(对于前向、随机等)。
标签: c++ iterator language-lawyer ppl