Что на самом деле делает std::vector::swap?

Что вызвало этот вопрос, так это некоторый код в строке:

std::vector<int> x(500);
std::vector<int> y;

std::swap(x,y);

И мне было интересно, требуется ли для их замены вдвое больше памяти, чем нужно x.

На cppreference я нашел для std::vector::swap (это метод, который эффективно вызывает последняя строка):

Обменяет содержимое контейнера на содержимое другого. Не вызывает никаких операций перемещения, копирования или замены отдельных элементов.

Все итераторы и ссылки остаются действительными. Итератор прошедшего конца становится недействительным.

И теперь я еще больше запутался, чем раньше. Что на самом деле делает std::vector::swap, когда он не перемещает, не копирует и не меняет местами элементы? Как возможно, что итераторы остаются действительными?

Означает ли это, что что-то вроде этого является допустимым кодом?

std::vector<int> x(500);
std::vector<int> y;
auto it = x.begin();
std::swap(x,y);
std::sort(it , y.end()); // iterators from different containers !!
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
1 487
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

vector внутренне хранит (по крайней мере) указатель на фактическое хранилище для элементов, размер и емкость. std::swap просто меняет местами указатели, размер и емкость (и вспомогательные данные, если они есть) вокруг; никакого удвоения памяти или копий элементов не делается, потому что указатель в x становится указателем в y и наоборот, без выделения новой памяти.

Итераторы для vector обычно представляют собой легкие оболочки вокруг указателей на базовую выделенную память (поэтому изменения емкости обычно делают итераторы недействительными), поэтому итераторы, созданные для x до свопа, продолжают ссылаться на y после свопа; ваш пример использования sort является законным и сортирует y.

Если вы хотите поменять местами сами элементы без замены хранилища (гораздо более дорогая операция, но которая оставляет существующие итераторы для x, ссылающихся на x), вы можете использовать std::swap_range, но это относительно редкий вариант использования. Использование памяти для этого будет зависеть от реализации swap для базового объекта; по умолчанию это часто связано с временным, но только для одного из объектов, которые заменяются за раз, а не для всего одного vector.


Судя по комментариям, он может эквивалентно использовать указатели на конец используемого пространства и конец емкости, но любой подход логически эквивалентен и просто микрооптимизируется в пользу немного разных ожидаемых вариантов использования; хранение всех указателей оптимизирует использование итераторов (разумный выбор), а сохранение size_type оптимизирует вызовы .size()/.capacity().

Глядя на Стандарт, я изо всех сил пытаюсь найти что-нибудь о гарантированной достоверности итераторов. Вы можете что-нибудь добавить по этому поводу?

BoBTFish 10.12.2020 16:52

В большинстве реализаций, которые я видел - у вектора всего 3 указателя - __ begin, __end, __end_of_capacity - где size == __end - __begin; емкость == __end_of_capacity - __begin

PiotrNycz 10.12.2020 16:53

@BoBTFish - Так и должно быть timsong-cpp.github.io/cppwp/n4861/…

StoryTeller - Unslander Monica 10.12.2020 16:54

@PiotrNycz: Конечно, это правильный альтернативный подход; Дело в том, что нет никаких обстоятельств, при которых он прибегал бы к созданию полной копии базового хранилища/элементов одного из рассматриваемых vector, и на практике для этого требуется как минимум три значения (по крайней мере, один тип указателя и два это могут быть типы указателей или size_type, в зависимости от выбора реализации). В любом случае, это три быстрых обмена указателями или size_type, независимо от размера vector или типа шаблона, который он хранит.

ShadowRanger 10.12.2020 16:55

@StoryTeller-UnslanderMonica: я добавил эту ссылку; Спасибо!

ShadowRanger 10.12.2020 16:56

Мне приходит в голову, что технически реализация vector может не нуждаться в хранении собственной емкости. Если он привязан к той же stdlib, что и распределитель, и может полагаться на внутренние компоненты распределителя, то в самом распределении хранится емкость распределения (обычно вставляется непосредственно перед возвращаемым указателем), и vector может использовать это (например, запросить место для x элементы, но использовать все, что есть, живые ссылки на метаданные распределения). vector может обойтись только указателем массива и размером; у него действительно есть способности, но не как часть членов класса.

ShadowRanger 10.12.2020 17:14

Но какая польза от замены x-y ? Это только удобочитаемые имена переменных, и это не должно иметь никакого влияния на сгенерированный машинный код.

Max Power 02.07.2022 08:13

Я напишу игрушечный вектор.

struct toy_vector {
  int * buffer = 0;
  std::size_t valid_count = 0;
  std::size_t buffer_size = 0;

  int* begin() { return buffer; }
  int* end() { return buffer+valid_count; }
  std::size_t capacity() const { return buffer_size; }
  std::size_t size() const { return valid_count; }

  void swap( toy_vector& other ) {
    std::swap( buffer, other.buffer );
    std::swap( valid_count, other.valid_count );
    std::swap( buffer_size, other.buffer_size );
  }

Это в основном все. Я реализую несколько методов, чтобы вы увидели, что у нас достаточно инструментов для работы:

  int& operator[](std::size_t i) { return buffer[i]; }

  void reserve(int capacity) {
    if (capacity <= buffer_size)
      return;
    toy_vector tmp;
    tmp.buffer = new int[capacity];
    for (std::size_t i = 0; i < valid_count; ++i)
      tmp.buffer[i] = std::move(buffer[i]);
    tmp.valid_count = valid_count;
    tmp.buffer_size = capacity;
    swap( tmp );
  }
  void push_back(int x) {
    if (valid_count+1 > buffer_size) {
      reserve( (std::max)((buffer_size*3/2), buffer_size+1) );
    buffer[valid_count] = std::move(x);
    ++valid_count;
  }
  // real dtor is more complex.
  ~toy_vector() { delete[] buffer; }
};

фактические векторы имеют проблемы с безопасностью исключений, больше беспокоятся о времени жизни объекта и распределителях (я использую целые числа, поэтому мне все равно, правильно ли я их создаю/уничтожаю), и могут хранить 3 указателя вместо указателя и 2 размера. Но поменять местами эти 3 указателя так же просто, как поменять местами указатель и 2 размера.

Итераторы в реальных векторах, как правило, не являются необработанными указателями (но могут быть). Как вы можете видеть выше, итераторы необработанных указателей на вектор a становятся итераторами необработанных указателей на вектор b, когда вы выполняете a.swap(b); несырые итераторы векторов указателей в основном представляют собой причудливые обернутые указатели и должны следовать той же семантике.

Стандарт C++ явно не предписывает реализацию, которая выглядит так, но он был основан на реализации, которая выглядит так, и неявно требует реализации, которая почти идентична этой (я уверен, что кто-то мог бы придумать умный стандарт совместимый вектор, который не выглядит так, но каждый вектор в каждой стандартной библиотеке, которую я видел, выглядел так.)

Другие вопросы по теме