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

Using an iterator to Divide an Array into Parts with Unequal Size

本文关键字:大小不等 划分 迭代器 数组      更新时间:2023-10-16

我有一个数组,我需要将其划分为3元素子数组。我想用迭代器来做到这一点,但我最终迭代了数组的末尾并出现了段错误,即使我没有取消引用迭代器。给定:我正在做的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; });

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

您看到的段错误来自next为您检查范围是调试实现中的断言,用于检查未定义的行为。迭代器和指针的行为不会超出其分配的范围,并且"一个过去结束"元素:迭代器是否超过"一个过去结束"迭代器未定义的行为?

这意味着递增超过"一个过去结束"元素是与迭代器的后续使用无关的未定义行为。为了定义行为,您必须使用整数模算法或类似解决方案,但您必须auto it = next(bar, 3)更改为根据至少子数组大小的可用性进行条件化的东西,因此:auto it = size(foo) <= 3 ? finish : next(bar, 3) .

在可用的情况下,这里的最佳解决方案将导致最少冗余的迭代是将容器中剩余的大小作为整数进行跟踪,当它超出范围和"一个过去结束"时,该整数不会受到未定义行为的影响。这可以通过以下方式实现:

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;

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

问题是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; });

现场示例

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

禁止这样做的原因在您的另一个问题中得到了很好的介绍 迭代器是否已经过了"一个过去-结束"迭代器未定义的行为? 所以我只讨论改进的解决方案。

对于随机访问迭代器(如果您使用 <,则必须具有(,不需要任何昂贵的模运算。

突出的一点是:

    it + stride在接近
  • 尾声时it失败
  • 如果容器包含的元素太少,则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;
}

对于通过数组分区完成此迭代的最有效方法存在一些分歧。

首先是一次性整数模法,除了我的答案中的更改之外,还必须定义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 行汇编,最值得注意的是条件it != finish生成以下指令:

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

其次,使用 Ben Voigt 的 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 行汇编,最明显的是 try_advance 中的条件:if (end - it < stride) return false;每次迭代生成代码:

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

在了解到cmpq实际上只是一个减法和比较操作后,我编写了一些基准代码:http://coliru.stacked-crooked.com/a/ad869f69c8dbd96f 我需要使用 Coliru 才能启用优化,但它一直给我虚假的测试计数增量,我不确定那里发生了什么。我能说的是局部的,重复的迭代器减法总是更快,有时甚至显着。在了解到这一点后,我相信本·福格特的答案应该被标记为正确的答案。

编辑:

我有一个有趣的发现。首先走的算法总是松散的。我已经重写了代码以在每次传递时交换第一个算法。完成此操作后,整数模方法总是击败迭代器减法方法,这是通过查看程序集所怀疑的那样,Coliru再次发生了一些可疑的事情,但是您可以获取此代码并在本地运行它:http://coliru.stacked-crooked.com/a/eb3e0c70cc138ecf


下一个问题是这两种算法都是懒惰的;如果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