15

vector::insert(dst_iterator, src_begin, src_end)(插入范围)可以针对随机访问迭代器进行优化,以src_end - src_begin首先保留所需的容量,然后执行复制。

我的主要问题是:标准是否还允许vector::insert避免对每个复制元素进行容量检查?(即不在push_back要插入的每个元素上使用或类似)

我将避免这种容量检查称为“优化insert”。


可能出了什么问题:我可以想象一个迭代器在取消引用时会产生副作用

注意:标准保证传递给的迭代器insert将被取消引用一次(见问题结尾)。

#include <vector>
#include <iterator>
#include <iostream>

template < typename T >
struct evil_iterator : std::iterator < std::random_access_iterator_tag, T >
{
    using base = std::iterator < std::random_access_iterator_tag, T >;

    std::vector<T>* evil_feedback;
    typename std::vector<T>::iterator innocent_iterator;

    evil_iterator( std::vector<T>* c,
                   typename std::vector<T>::iterator i )
        : evil_feedback{c}
        , innocent_iterator{i}
    {}

    void do_evil()
    {
        std::cout << "trying to do evil; ";
        std::cout << "cap: " << evil_feedback->capacity() << ", ";
        std::cout << "size: " << evil_feedback->size() << ", ";

        // better not invalidate the iterators of `*evil_feedback`
        // passed to the `insert` call (see example below)
        if( evil_feedback->capacity() > evil_feedback->size() )
        {
            evil_feedback->push_back( T{} );
            // capacity() might be == size() now
            std::cout << "successful >:]" << std::endl;
        }else
        {
            std::cout << "failed >:[" << std::endl;
        }
    }

    T& operator*()
    {
        do_evil();  // <----------------------------------------
        return *innocent_iterator;
    }


    // non-evil iterator member functions-----------------------

    evil_iterator& operator++()
    {
        ++innocent_iterator;
        return *this;
    }
    evil_iterator& operator++(int)
    {
        evil_iterator temp(*this);
        ++(*this);
        return temp;
    }


    evil_iterator& operator+=(typename base::difference_type p)
    {
        innocent_iterator += p;
        return *this;
    }
    evil_iterator& operator-=(typename base::difference_type p)
    {
        innocent_iterator -= p;
        return *this;
    }

    evil_iterator& operator=(evil_iterator const& other)
    {
        evil_feedback = other.evil_feedback;
        innocent_iterator = other.innocent_iterator;
        return *this;
    }

    evil_iterator operator+(typename base::difference_type p)
    {
        evil_iterator temp(*this);
        temp += p;
        return temp;
    }
    evil_iterator operator-(typename base::difference_type p)
    {
        evil_iterator temp(*this);
        temp -= p;
        return temp;
    }

    typename base::difference_type operator-(evil_iterator const& p)
    {
        return this->innocent_iterator - p.innocent_iterator;
    }

    bool operator!=(evil_iterator const& other) const
    {  return innocent_iterator != other.innocent_iterator;  }
};

例子:

int main()
{
    std::vector<int> src = {3, 4, 5, 6};
    std::vector<int> dst = {1, 2};

    evil_iterator<int> beg = {&dst, src.begin()};
    evil_iterator<int> end = {&dst, src.end()};

    // explicit call to reserve, see below
    dst.reserve( dst.size() + src.size() );
    // using dst.end()-1, which stays valid during `push_back`,
    //   thanks to Ben Voigt pointing this out
    dst.insert(dst.end()-1, beg, end);  // <--------------- doing evil?

    std::copy(dst.begin(), dst.end(), 
              std::ostream_iterator<int>{std::cout, ", "});
}

问题:

  1. 可以vector::insert优化以避免对每个插入元素进行容量检查吗?
  2. 仍然是evil_iterator有效的迭代器吗?
  3. 如果是这样,是evil_iterator 邪恶insert的,即如果如上所述进行优化,它会导致 UB / 不合规行为吗?

也许我do_evil的不够邪恶..在clang++ 3.2上没有问题(使用libstdc++):

编辑 2:添加了对reserve. 现在,我在做坏事:)

试图作恶;cap: 6, size: 2, 成功 >:]
试图作恶;cap: 6, size: 3, 成功 >:]
试图作恶;cap: 6, size: 4, 成功 >:]
试图作恶;上限:6,大小:9,失败 >:[
1, 3, 4, 5, 6, 0, 0, 135097, 2,

编辑:为什么我认为优化可以打破这一点:

  1. 一开始就考虑dst.size() == dst.capacity() == 2
  2. 调用insert需要新容量 6。
  3. 优化将容量精确地扩大到 6,然后通过从src迭代器 ( beg, end) 复制来开始插入元素。
  4. 此复制在不进行容量检查的循环内完成。(这就是优化。)
  5. 在复制过程中,更多元素被添加到向量中(不使迭代器无效),在do_evil. 现在的容量不再足以容纳要复制的其余元素。

也许您必须reserve在示例中明确使用以强制capacity在使用之前更新可观察对象do_evil。目前,insert可以保留一些容量,但capacity只有在复制完成后才能更改返回的内容(即可观察容量)。


到目前为止,我在标准中发现的似乎允许优化insert

[sequence.reqmts]/3

a.insert(p,i,j)[...]

要求:T 应该是 EmplaceConstructible 从 *i 到 X 的。

对于向量,如果迭代器不满足前向迭代器要求(24.2.5),T 也应该是 MoveInsertable 到 X 和 MoveAssignable。[i,j) 范围内的每个迭代器都应该被取消引用一次。

pre: i 和 j 不是 a 的迭代器。在 p 之前插入 [i, j) 中元素的副本

[vector.modifiers] 开启insert

1 备注:如果新大小大于旧容量,则导致重新分配。如果没有发生重新分配,则插入点之前的所有迭代器和引用仍然有效。如果异常被 T 的复制构造函数、移动构造函数、赋值运算符或移动赋值运算符或任何 InputIterator 操作引发,则没有任何影响。如果非 CopyInsertable T 的移动构造函数抛出异常,则未指定效果。

2 复杂度:复杂度与插入的元素数量加上到向量末端的距离成线性关系。

4

3 回答 3

4

再看一遍,我认为这条规则(第 17.6.4.9 节)更明确地禁止了你试图做的事情:

除非另有明确说明,否则以下各项均适用于 C++ 标准库中定义的函数的所有参数。

  • 如果函数的参数具有无效值(例如函数域之外的值或对其预期用途无效的指针),则行为未定义。

我认为这条规则适用于函数调用的整个过程,而不仅仅是函数入口。

此外,push_back()保证 (23.3.7.5):

如果没有发生重新分配,则插入点之前的所有迭代器和引用仍然有效。

position传递给insert,这是在调用dst.end()之前评估的insert,不在第一次调用的插入点之前evil_feedback->push_back(),因此它不会保持有效(您在这里小心避免重新分配的事实并不能拯救您,因为您只满足了一半的条件)。这意味着您传递给std::vector::insertC++ 标准库中定义的函数的参数在该调用期间无效,从而使您直接进入未定义行为的领域。


上一个答案:

我认为你违反了你引用的这个先决条件:

pre:i并且j不是a.

于 2013-05-20T23:26:54.860 回答
1

(注意:这更多是评论,我使用答案来允许格式化和更长的内容。标记 CW 因为评论不应该收到代表)

如果输入迭代器是随机访问的,我相信这是一个避免 O(NM) 复杂度的正确算法:

  1. 确定要插入的范围的大小(仅适用于随机访问迭代器)。
  2. 预留额外空间。
  3. 调整大小。
  4. 移动构造新的尾部元素。
  5. 将其他中间元素移向新端。
  6. 将源元素复制到移动后留空的范围内。
于 2013-05-21T02:17:05.880 回答
0

以下是我的看法:

  1. 是的; 取消引用可能会对您的向量产生副作用(例如),这在某些情况下可能会导致未定义的行为,但对于符合标准的迭代器来说,情况不应如此。
  2. 不; 迭代器旨在作为指针的泛化 - 因为指针取消引用可能没有副作用(找不到引用),迭代器应该也是如此 [iterator.requirements.general]。鉴于这种解释,“插入优化”(1)是有效的。
于 2013-05-20T23:20:54.680 回答