Является ли vector:: insert разрешено резервировать только один раз и избежать дополнительных проверок емкости?

vector::insert(dst_iterator, src_begin, src_end) (вставить диапазон) можно оптимизировать для итераторов с произвольным доступом, чтобы сначала зарезервировать требуемую емкость src_end - src_begin, а затем выполнить копию.

Основной вопрос У меня есть: позволяет ли Стандарт также vector::insert избегать проверки емкости для каждого скопированного элемента? (I.e. не используя 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, ", "});
}

Вопросы:

  • Можно ли оптимизировать vector::insert, чтобы избежать проверки емкости для каждого вставленного элемента?
  • Является ли evil_iterator допустимым итератором?
  • Если это так, это evil_iterator зло, т.е. может ли это привести к UB/несоблюдению поведения, если insert оптимизирован, как описано выше?

Возможно, мой do_evil не является достаточно злым.. не имеет проблем с clang++ 3.2 (используя libstdС++):

Изменить 2: добавлен вызов reserve. Теперь я делаю зло:)

пытается сделать зло; колпачок: 6, размер: 2, успешный > :]
пытаясь сделать зло; колпачок: 6, размер: 3, успешный > :]
пытаясь сделать зло; колпачок: 6, размер: 4, успешный > :]
пытаясь сделать зло; колпачок: 6, размер: 9, не удалось > : [
1, 3, 4, 5, 6, 0, 0, 135097, 2,

Изменить: почему я думаю, что оптимизация может нарушить это:

  • Рассмотрим dst.size() == dst.capacity() == 2 в начале.
  • Для вызова insert требуется новая емкость 6.
  • Оптимизация увеличивает емкость до точности 6, а затем начинает вставлять элементы путем копирования из итераторов src (beg, end).
  • Это копирование выполняется в цикле, где не происходит проверки емкости. (Это оптимизация.)
  • В процессе копирования в вектор добавляются дополнительные элементы (без признания итераторов), в do_evil. Теперь емкость недостаточна для хранения остальных элементов, которые нужно скопировать.

Возможно, вам пришлось использовать reserve в явном виде для принудительного обновления наблюдаемого capacity перед использованием do_evil. В настоящее время insert может зарезервировать некоторую емкость, но изменить то, что возвращает capacity (т.е. Наблюдаемая емкость) только после завершения копирования.


То, что я нашел в Стандарте, похоже, позволяет оптимизировать insert:

[sequence.reqmts]/3

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

Требуется: T должен быть EmplaceConstructible в X из * i.

Для вектора, если итератор не удовлетворяет требованиям прямого итератора (24.2.5), T также должен быть MoveInsertable в X и MoveAssignable. Каждый итератор в диапазоне [i, j) должен быть разыменован ровно один раз.

pre: я и j не являются итераторами в a. Вставляет копии элементов в [i, j) до p

[vector.modifiers] на insert

1 Примечания: вызывает перераспределение, если новый размер больше старой. Если перераспределение не происходит, все итераторы и ссылки до точки вставки остаются в силе. Если исключение выбрано иначе, чем конструктор копирования, переместите конструктор, оператор присваивания или оператор присваивания перемещения T или любую операцию InputIterator, никаких эффектов нет. Если исключение вызывается конструктором перемещения не-CopyInsertable T, эффекты не заданы.

2 Сложность: сложность линейна по числу вставленных элементов плюс расстояние до конца вектора.

Ответы

Ответ 1

Глядя снова, я думаю, что это правило (раздел 17.6.4.9) является более четким запретом на то, что вы пытались сделать:

Каждое из следующих относится ко всем аргументам к функциям, определенным в стандартной библиотеке С++, если явно не указано иначе.

  • Если аргумент функции имеет недопустимое значение (например, значение вне домена функции или указатель недействителен для его предполагаемого использования), поведение undefined.

Я думаю, что это правило применяется в течение всей продолжительности вызова функции, а не только при вводе функции.

Кроме того, push_back() гарантирует, что (23.3.7.5):

Если перераспределение не происходит, все итераторы и ссылки до точки вставки остаются в силе.

Ваш position передан insert, который равен dst.end(), который был оценен перед вызовом insert, не находится перед точкой вставки первого вызова evil_feedback->push_back(), поэтому он не остается в силе (факт что вы тщательно избегаете перераспределения здесь, не спасает вас, так как вы только встретили половину этого условия). Это означает, что аргумент, который вы передали std::vector::insert, функция, определенная в стандартной библиотеке С++, недействительна в течение продолжительности этого вызова, вы приземляетесь прямо в области поведения undefined.


Предыдущий ответ:

Я думаю, что вы нарушили это предварительное условие, которое вы указали:

pre: i и j не являются итераторами в a.

Ответ 2

(Примечание: это скорее комментарий, я использую ответ, чтобы разрешить форматирование и более длительный контент. Маркировка CW, потому что комментарии не должны получать rep)

Я считаю, что это правильный алгоритм, который позволяет избежать сложности O (NM), если входные итераторы являются случайным доступом:

  • Определите размер диапазона для вставки (возможно только для итераторов с произвольным доступом).
  • зарезервировать дополнительное пространство.
  • Отрегулируйте размер.
  • Переместить-построить новые элементы хвоста.
  • Переместить - назначить другие промежуточные элементы к новому концу.
  • Скопируйте исходные элементы в диапазон, оставшийся пустым по ходу.

Ответ 3

Вот мои взгляды:

  • Да; de-referencing может иметь побочные эффекты для вашего вектора (пример), который может привести к поведению undefined в некоторых случаях, но это не должно быть в случае со стандартными итераторами.
  • Нет; Итераторы предназначены для обобщения указателей - поскольку у указателей de-referencing не могут быть побочные эффекты (не удается найти ссылку), то то же самое должно быть в случае итераторов [iterator.requirements.general]. Учитывая эту интерпретацию, "оптимизация вставки" (1) действительна.