Почему элементы списка stl:: list копируются в список?

Документация по стандартной библиотеке шаблонов для list говорит:

void push_back (const T & x);

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

Эти семантики сильно отличаются от семантики Java и путают меня. Есть ли в STL принцип дизайна, который мне не хватает? "Копировать данные все время"? Это меня пугает. Если я добавлю ссылку на объект, почему объект скопирован? Почему не только объект прошел?

Здесь должно быть решение для определения языка, но большая часть комментариев, которые я нашел в Qaru и других сайтах, сосредотачивается на проблемах сбрасывания исключений, связанных с тем, что все это копирование объектов может вызывать исключения. Если вы не копируете и просто обрабатываете ссылки, все эти проблемы с исключениями исчезают. Очень смутно.

Обратите внимание: в этой старой базе кода, с которой я работаю, boost не является вариантом.

Ответ 1

STL всегда сохраняет именно то, что вы говорите, чтобы хранить его. list<T> всегда является списком T, поэтому все будет храниться по значению. Если вам нужен список указателей, используйте list<T*>, который будет похож на семантику в Java.

Это может заставить вас попробовать list<T&>, но это невозможно. Ссылки на С++ имеют разную семантику, чем ссылки в Java. В С++ ссылки должны быть инициализированы, чтобы указывать на объект. После инициализации ссылки он всегда укажет на этот объект. Вы никогда не сможете указать на другой объект. Это делает невозможным наличие контейнера ссылок в С++. Ссылки на Java более тесно связаны с указателями на С++, поэтому вместо этого следует использовать list<T*>.

Ответ 2

Он называется "семантикой значений". С++ обычно кодируется для копирования значений, в отличие от Java, где, в основном, примитивные типы, вы копируете ссылки. Это может вас напугать, но лично Java-семантика меня пугает больше. Но в С++ у вас есть выбор, если вы хотите, чтобы эталонная семантика использовала только указатели (предпочтительные умные). Тогда вы будете ближе к Java, к которому вы привыкли. Но помните в С++ никакой сборке мусора (именно поэтому вы должны обычно использовать интеллектуальные указатели).

Ответ 3

Вы не добавляете ссылку на объект. Вы передаете объект по ссылке. Это другое. Если вы не прошли по ссылке, дополнительная копия могла быть сделана еще до фактической вставки.

И он копирует, потому что вам нужна копия, иначе код вроде:

std::list<Obj> x;
{
   Obj o;
   x.insert(o);
}

оставит список с недопустимым объектом, потому что o вышел из области видимости. Если вы хотите что-то похожее на Java, рассмотрите возможность использования shared_ptr. Это дает вам преимущества, с которыми вы привыкли в Java: автоматическое управление памятью и легкое копирование.

Ответ 4

Java работает точно так же. Позвольте мне объяснить:

Object obj = new Object();
List<Object> list = new LinkedList<Object>();
list.add(obj);

Каков тип obj? Это ссылка на Object. Фактический объект плавает где-то в куче - единственное, что вы можете сделать в Java, - это обход ссылок на него. Вы передаете ссылку на объект методу списка add, и список хранит копию этой ссылки сам по себе. Вы можете позже изменить именованную ссылку obj, не затрагивая отдельную копию этой ссылки, хранящуюся в списке. (Конечно, если вы изменяете сам объект, вы можете увидеть это изменение через любую ссылку.)

У С++ есть больше опций. Вы можете эмулировать Java:

class Object {};
// ...
Object* obj = new Object;
std::list<Object*> list;
list.push_back(obj);

Каков тип obj? Это указатель на Object. Когда вы передаете его методу списка push_back, список сохраняет копию этого указателя сам по себе. Это имеет ту же семантику, что и Java.

Но если вы думаете об этом с точки зрения эффективности... насколько велика ссылка на С++-указатель/Java? 4 байта или 8 байтов, в зависимости от вашей архитектуры. Если объект, о котором вы заботитесь, находится вокруг этого размера или меньше, почему бы его не помещать в кучу, а затем передавать указатели на него повсюду? Просто передайте объект:

class Object {};
// ...
Object obj;
std::list<Object> list;
list.push_back(obj);

Теперь obj является фактическим объектом. Вы передаете его в список push_back, который хранит копию этого объекта сам по себе. Это идиома С++, в некотором роде. Мало того, что это имеет смысл для небольших объектов, где указатель является чистым накладными расходами, он также упрощает работу на языке, отличном от GC (там ничего не лежит на куче, которая может быть случайно просочилась), и если срок жизни объекта естественно связан в список (т.е. если он удален из списка, то семантически он больше не должен существовать), тогда вы можете также сохранить весь объект в списке. Он также обладает преимуществами локализации кеша (при использовании в std::vector, во всяком случае).


Вы можете спросить: "Почему push_back принимает ссылочный аргумент?" Для этого есть достаточно простая причина. Каждый параметр передается по значению (опять же, как на С++, так и на Java). Если у вас есть std::list of Object*, отлично, вы передаете указатель и копируете этого указателя и передаете его в функцию push_back. Затем внутри этой функции создается другая копия этого указателя и сохраняется в контейнере.

Это прекрасно для указателя. Но в С++ копирующие объекты могут быть произвольно сложными. Конструктор копирования может сделать что угодно. В некоторых случаях копирование объекта дважды (один раз в функцию и снова в контейнер) может быть проблемой производительности. Таким образом, push_back принимает свой аргумент по const-ссылке - он делает единственную копию, прямо из исходного объекта в контейнер.

Ответ 5

Без подсчета ссылок нет способа поддерживать совместное владение, поэтому однократное владение поддерживается копированием.

Рассмотрим обычный случай, когда вы хотите добавить объект, выделенный стеком, в список, который его переживет:

void appendHuzzah(list<string> &strs) {
  strs.push_back(string("huzzah!"));
}

Список не может сохранить исходный объект, потому что этот объект будет уничтожен, когда он выпадет из области видимости. При копировании список получает свой собственный объект, чей жизненный цикл полностью находится под его контролем. Если бы это было иначе, такое прямое использование потерпело бы крах и было бы бесполезным, и нам всегда приходилось использовать списки указателей.

Java различает примитивные типы и ссылочные типы. В С++ все типы примитивны.