【问题标题】:What would be a good implementation of iota_n (missing algorithm from the STL)iota_n 的一个好的实现是什么(STL 中缺少算法)
【发布时间】:2012-08-01 21:00:30
【问题描述】:

在 C++11 中,STL 现在有一个 std::iota 函数(参见 reference)。然而,与std::fill_nstd::generate_n 相比,没有std::iota_n。什么是一个好的实现呢?直接循环(备选方案 1)或使用简单 lambda 表达式(备选方案 2)对std::generate_n 的委托?

备选方案 1)

template<class OutputIterator, class Size, class T>
OutputIterator iota_n(OutputIterator first, Size n, T value)
{
        while (n--)
                *first++ = value++;
        return first;
}

备选方案 2)

template<class OutputIterator, class Size, class T>
OutputIterator iota_n(OutputIterator first, Size n, T value)
{
        return std::generate_n(first, n, [&](){ return value++; });
}    

两种替代方案是否会通过优化编译器生成等效代码?

更新:结合了@Marc Mutz 的优点,在其目标点也返回了迭代器。这也是 std::generate_n 在 C++11 中与 C++98 相比的更新方式。

【问题讨论】:

  • 我认为这个问题的重点是一些过于具体的东西,而不是更笼统的东西:不同的循环结构。
  • 何不试试,对比一下汇编器?
  • @KerrekSB 在挖掘装配输出方面并不是什么专家。我很想听听具有这种专业知识的人的意见,如果带有 lambda 的 STL oneliners 通常会被优化为直循环。如果是这样的话,这将是一个更大的动力来编写更多关于 STL 算法的变体,而不是努力思考复杂的循环。
  • 如果你只想使用随机访问迭代器,你可以简单地使用std::iota(start, start + n, value);。另外,我会将i != n 更改为i &lt; n 作为第一个替代方案。

标签: c++ algorithm stl c++11 iota


【解决方案1】:

作为一个随机示例,我使用g++ -S -O2 -masm=intel (GCC 4.7.1, x86_32) 编译了以下代码:

void fill_it_up(int n, int * p, int val)
{
    asm volatile("DEBUG1");
    iota_n(p, n, val);
    asm volatile("DEBUG2");
    iota_m(p, n, val);
    asm volatile("DEBUG3");
    for (int i = 0; i != n; ++i) { *p++ = val++; }
    asm volatile("DEBUG4");
}

这里iota_n 是第一个版本,iota_m 是第二个版本。在所有三种情况下,程序集都是这样的:

    test    edi, edi
    jle .L4
    mov edx, eax
    neg edx
    lea ebx, [esi+edx*4]
    mov edx, eax
    lea ebp, [edi+eax]
    .p2align 4,,7
    .p2align 3
.L9:
    lea ecx, [edx+1]
    cmp ecx, ebp
    mov DWORD PTR [ebx-4+ecx*4], edx
    mov edx, ecx
    jne .L9

使用-O3,三个版本也非常相似,但要长很多(使用条件移动和punpcklqdq 等)。

【讨论】:

  • 谢谢,这是一个很好的答案。不管 punpcklqdq 做什么,(我在 MSDN 上查看过),令人欣慰的是,调用 std::generate_n + lambda 几乎没有任何抽象损失。
【解决方案2】:

您太专注于代码生成,以至于忘记了正确的界面。

你正确地要求OutputIterator,但如果你想再次调用它会发生什么?

list<double> list(2 * N);
iota_n(list.begin(), N, 0);
// umm...
iota_n(list.begin() + N, N, 0); // doesn't compile!
iota_n(list.rbegin(), N, 0); // works, but create 0..N,N-1..0, not 0..N,0..N
auto it = list.begin();
std::advance(it, N);
iota_n(it, N, 0); // works, but ... yuck and ... slow (O(N))

iota_n 内部,你仍然知道你在哪里,但是你已经把这些信息扔掉了,所以调用者无法在恒定时间内得到它。

一般原则:不要丢弃有用的信息。

template <typename OutputIterator, typename SizeType, typename ValueType>
auto iota_n(OutputIterator dest, SizeType N, ValueType value) {
    while (N) {
        *dest = value;
        ++dest;
        ++value;
        --N;
    }
    // now, what do we know that the caller might not know?
    // N? No, it's zero.
    // value? Maybe, but it's just his value + his N
    // dest? Definitely. Caller cannot easily compute his dest + his N (O(N))
    //       So, return it:
    return dest;
}

有了这个定义,上面的例子就变得简单了:

list<double> list(2 * N);
auto it = iota_n(list.begin(), N, 0);
auto end = iota_n(it, N, 0);
assert(end == list.end());

【讨论】:

  • 很好,我同意现有的iota 和推定的iota_n 都应该返回目的地。
  • @TemplateRex: iota 不返回目的地,因为它等于它的第二个参数。但是iota_n不同,终点是隐含的,因此iota_n确实需要返回目的地。
  • 谢谢,现在我明白了。我用你的信息更新了答案。请注意,std::generate_n 在 C++11 中也进行了此升级。
  • 这里有一点我不喜欢,N,按照惯例是宏名称,被用作变量。其余的都很好。特别是,使用后缀增量魔法来混淆代码是没有意义的。写出赋值和增量操作并没有什么坏处。
【解决方案3】:

一个假设的iota_n

std::iota_n(first, count, value)

可以用一个衬里代替。

std::generate_n(first, count, [v=value]()mutable{return v++;})

我更喜欢它有一个不在标准中的挥之不去的功能。话虽如此,我认为std::iota_n 应该在标准中。

【讨论】:

    【解决方案4】:

    可能将 back_insertor 与 std::generate_n 一起使用,以避免预先分配集合。

    vector<int> v3;
    generate_n(back_inserter(v3),10,[i=1]() mutable{
        return i++;
    });
    

    我们可以把它换成 iota_n 直到我们得到 iota_n :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-09-12
      • 1970-01-01
      • 1970-01-01
      • 2011-03-27
      • 2019-07-15
      • 1970-01-01
      • 2012-08-12
      • 1970-01-01
      相关资源
      最近更新 更多