使用迭代器将数组划分为大小不等的部分

时间:2016-04-05 11:40:37

标签: c++ iterator modulo termination data-partitioning

我有一个数组,我需要将其分成3个元素的子数组。我想用迭代器来做这件事,但我最终迭代过了数组的末尾并且segfaulting ,即使我没有取消引用迭代器。给定:auto foo = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };我正在做:

auto bar = cbegin(foo);

for (auto it = next(bar, 3); it < foo.end(); bar = it, it = next(bar, 3)) {
    for_each(bar, it, [](const auto& i) { cout << i << endl; });
}

for_each(bar, cend(foo), [](const auto& i) { cout << i << endl; });

现在我可以通过定义finish迭代器来解决这个问题:

auto bar = cbegin(foo);
auto finish = next(cend(foo), -(size(foo) % 3));

for (auto it = next(bar, 3); it != finish; bar = it, it = next(bar, 3)) {
    for_each(bar, it, [](const auto& i) { cout << i << endl; });
}

for_each(bar, finish, [](const auto& i) { cout << i << endl; });
for_each(finish, cend(foo), [](const auto& i) { cout << i << endl; });

但是当我没有取消引用迭代器时,这似乎是不必要的。为什么我不能做第一个版本?

3 个答案:

答案 0 :(得分:2)

您看到的段错误来自next检查您的范围是您的Debug实现中的断言,以检查未定义的行为。迭代器和指针的行为没有定义超出它们分配的范围,并且&#34;一个过去的结束&#34; element:Are iterators past the "one past-the-end" iterator undefined behavior?

这意味着增加超过&#34;一个过去的结束&#34;元素是未定义的行为独立于迭代器的后续使用。为了定义行为,您必须使用类似Integer Modulo算法的解决方案,但您必须根据至少大小的可用性将auto it = next(bar, 3)更改为条件化的内容您的子数组,如:auto it = size(foo) <= 3 ? finish : next(bar, 3)

在可行的情况下,这里最好的解决方案是导致冗余度最小的迭代是将容器中剩余的大小跟踪为一个整数,当它超出范围时不会受到未定义的行为的影响,并且&#34;一个过去 - 的端&#34 ;.这可以通过以下方式实现:

auto bar = cbegin(foo);

for (auto i = size(foo); i > STEP; i -= STEP) {
    for(auto j = 0; j < STEP; ++j, ++bar) cout << *bar << '\t';
    cout << endl;
}

for(auto i = 0; j < STEP; ++j, ++bar) cout << *bar << '\t';
cout << endl;

编辑:我之前建议使用不是Debug条件的指针,这是未定义的行为。

问题是next正在为您检查范围。我们一直使用分配内存之外的指针,例如nullptrend,这里的所有it都是。如果你只是在这里使用C风格的指针运算,你会没事的:

auto bar = cbegin(foo);

for (auto it = bar + 3; it < cend(foo); bar = it, it = bar + 3) {
    for_each(bar, it, [](const auto& i) { cout << i << endl; });
}

for_each(bar, cend(foo), [](const auto& i) { cout << '\t' << i << endl; });

Live Example

或者,如果您在Release配置中运行,则应删除范围检查,以便您可以使用代码的第一个版本。

答案 1 :(得分:1)

这个被禁止的原因很好地涵盖了您的其他问题Are iterators past the "one past-the-end" iterator undefined behavior?,因此我只会提出改进的解决方案。

对于随机访问迭代器(如果使用<,则必须具有该迭代器),对于昂贵的模运算操作无需任何其他操作。

重点是:

    {li> it + strideit接近结束时失败 如果容器包含的元素太少,
  • end() - stride会失败
  • end() - it始终是合法的

从那里开始,通过简单的代数操作将it + stride < end()转换为法律形式(从双方减去it)。

最后的结果,我多次使用过:

for( auto it = c.cbegin(), end = c.cend(); end - it >= stride; it += stride )

如果内存模型是平坦的,编译器可以自由地优化它与预先计算的end - stride * sizeof(*it)进行比较--C ++行为的限制不适用于编译器将C ++转换为

如果您更喜欢使用命名函数而不是运算符,那么当然可以使用std::distance(it, end),但这只对随机访问迭代器有效。

为了与前向迭代器一起使用,你应该使用一些结合了增量和终止条件的东西,比如

struct less_preferred { size_t value; less_preferred(size_t v) : value(v){} };

template<typename Iterator>
bool try_advance( Iterator& it, less_preferred step, Iterator end )
{
     while (step.value--) {
         if (it == end) return false;
         ++it;
     }
     return true;
}

通过这个额外的重载,您将获得随机访问迭代器的有效行为:

template<typename RandomIterator>
auto try_advance( RandomIterator& it, size_t stride, RandomIterator end )
     -> decltype(end - it < stride) // SFINAE
{
     if (end - it < stride) return false;
     it += stride;
     return true;
}

答案 2 :(得分:0)

关于通过数组分区完成此迭代的最有效方法,有some disagreement

首先是一次性整数模数方法,除my answer中的更改外,还必须定义auto size,因为gcc尚不支持size

auto foo = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  
auto size = distance(cbegin(foo), cend(foo));
auto bar = cbegin(foo);
auto finish = prev(cend(foo), size % 3);

for(auto it = size <= 3 ? cend(foo) : next(bar, 3); it != finish; bar = it, it = next(bar, 3)) {
    for_each(bar, it, [](const auto& i) { cout << i << '\t'; });
    cout << endl;
}

for_each(bar, finish, [](const auto& i) { cout << i << '\t'; });
cout << endl;
for_each(finish, cend(foo), [](const auto& i) { cout << i << '\t'; });
cout << endl;

这会创建112 lines of assembly,最值得注意的是条件it != finish生成这些说明:

cmpq    %r12, %r13
je      .L19
movq    %r12, %rbx
jmp     .L10

第二个使用Ben Voigt's try_advance重复迭代器减法,但仅使用随机访问专门化,因为随机访问迭代器存在编译器冲突:

auto foo = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  
auto bar = cbegin(foo);

for (auto it = cbegin(foo), end = cend(foo); try_advance(it, 3, end); bar = it) {
    for_each(bar, it, [](const auto& i) { cout << i << '\t'; });
    cout << endl;
}

for_each(bar, cend(foo), [](const auto& i) { cout << i << '\t'; });
cout << endl;

这会创建119 lines of assembly,最值得注意的是try_advance中的条件:if (end - it < stride) return false;每次迭代都会产生代码:

movq    %r12, %rax
subq    %rbp, %rax
cmpq    $11, %rax
ja      .L3

在得知cmpq is really just a subtract and compare operation后我写了一些基准代码:http://coliru.stacked-crooked.com/a/ad869f69c8dbd96f我需要使用Coliru才能打开优化,但它一直给我伪造的测试计数增量时间,我不确定那里发生了什么。 我可以说是局部的,重复的迭代器减法总是更快,有时显着更快。在得知这一点后,我相信Ben Voigt's answer应该被标记为正确的。

修改

我发现了一个有趣的发现。这是首先总是失败的算法。我重写了代码以在每次传递时交换第一个算法。完成此操作后,整数模数方法总是胜过迭代器减法方法,因为通过查看程序集可能会出现这种情况,Coliru再次发生了一些问题,但您可以使用此代码并在本地运行它:http://coliru.stacked-crooked.com/a/eb3e0c70cc138ecf < / p>

下一个问题是这两种算法都很懒惰;如果size(foo)是3的倍数,则会在vector的末尾分配一个空的vector。这需要对整数模算法进行大量分支来解决,但只需对重复的迭代器减法算法进行最简单的更改。由此产生的算法实际上表现出相同的基准数,但为了简单起见,边缘转到重复的迭代器减法:

整数模算法:

auto bar = cbegin(foo);
const auto size = distance(bar, cend(foo));

if (size <= 3) {
    for_each(bar, cend(foo), [](const auto& i) { cout << i << '\t'; });
    cout << endl;
}
else {
    auto finish = prev(cend(testValues), (size - 1) % 3 + 1);

    for (auto it = next(bar, 3); it != finish; bar = it, advance(it, 3)) {
        for_each(bar, it, [](const auto& i) { cout << i << '\t'; });
        cout << endl;
    }

    for_each(bar, finish, [](const auto& i) { cout << i << '\t'; });
    cout << endl;
    for_each(finish, cend(foo), [](const auto& i) { cout << i << '\t'; });
    cout << endl;
}

重复迭代器减法算法:

auto bar = cbegin(foo);

for (auto it = cbegin(foo); distance(it, cend(foo)) > 3; bar = it) {
    advance(it, 3);
    for_each(bar, it, [](const auto& i) { cout << i << '\t'; });
    cout << endl;
}

for_each(bar, cend(foo), [](const auto& i) { cout << i << '\t'; });
cout << endl;

编辑:将剩余大小算法放入帽子

上面的整数模数和重复减法算法都不止一次地迭代输入序列,除了慢一点这不是那么严重,因为目前我们正在使用双向迭代器,但是我们的输入迭代器应该不能有资格获得双向迭代器,这将是非常昂贵的。与迭代器类型无关,每次10,000,000次测试平台迭代时,剩余大小算法都会击败所有挑战者:

auto bar = cbegin(foo);

for (auto i = size(foo); i > STEP; i -= STEP) {
    for(auto j = 0; j < STEP; ++j, ++bar) cout << *bar << '\t';
    cout << endl;
}

for(auto i = 0; j < STEP; ++j, ++bar) cout << *bar << '\t';
cout << endl;

我再次将我的本地测试复制到Coliru,这会产生奇怪的结果,但您可以在本地验证:http://coliru.stacked-crooked.com/a/361f238216cdbace