Можем ли мы сказать пока, чтобы скопировать конструкторы?

Конструкторы копирования традиционно использовались в программах на C ++. Однако я сомневаюсь, что есть веская причина для этого, начиная с C ++ 11.

Даже когда логике программы не нужно копировать объекты, копировать конструкторы (обычно по умолчанию) часто включались с единственной целью перераспределения объекта. Без конструктора копирования вы не сможете хранить объекты в std::vector или даже вернуть объект из функции.

Однако, начиная с C ++ 11, конструкторы перемещения отвечали за перераспределение объектов.

Другим вариантом использования конструкторов копирования было просто создание клонов объектов. Тем не менее, я вполне уверен, что .copy() или же .clone() метод лучше подходит для этой роли, чем конструктор копирования, потому что …

  1. Копирование объектов не является обычным делом. Конечно, иногда интерфейс объекта должен содержать метод «сделай себе копию», но только иногда. И когда это так, явное лучше, чем неявное.

  2. Иногда объект может выставить несколько разных .copy()-подобные методы, потому что в разных контекстах копию, возможно, придется создавать по-разному (например, меньше или глубже).

  3. В некоторых контекстах мы бы хотели .copy() методы для выполнения нетривиальных задач, связанных с логикой программы (увеличить счетчик или, возможно, создать новое уникальное имя для копии). Я не принял бы любой код, который имеет неочевидную логику в конструкторе копирования.

  4. Последнее, но не менее важное, .copy() При необходимости метод может быть виртуальным, что позволяет решить проблему нарезка.


Единственные случаи, когда я на самом деле хотел бы использовать конструктор копирования:

  • RAII-дескрипторы копируемых ресурсов (вполне очевидно)
  • Структуры, которые предназначены для использования как встроенные типы, такие как математические векторы или матрицы —
    просто потому, что они часто копируются и vec3 b = a.copy() слишком многословен

Примечание: я учел тот факт, что конструктор копирования необходим для CAS, но CAS нужен для operator=(const T&) который я считаю излишним, основываясь на точно таких же рассуждениях;
.copy() + operator=(T&&) = default было бы предпочтительным, если вам действительно это нужно.)

Для меня вполне достаточно стимула использовать T(const T&) = delete везде по умолчанию и предоставить .copy() метод, когда это необходимо. (Возможно, также private T(const T&) = default просто чтобы иметь возможность писать copy() или же virtual copy() без шаблонов.)

Q: Правильно ли приведенные выше рассуждения или я упускаю какие-либо веские причины, почему логические объекты действительно нуждаются или как-то выигрывают от конструкторов копирования?

В частности, я прав в том, что конструкторы перемещения полностью взяли на себя ответственность за перераспределение объектов в C ++ 11? Я неофициально использую «перераспределение» для всех ситуаций, когда объект должен быть перемещен в другое место в памяти без изменения его состояния.

2

Решение

Короткий ответ

Являются ли приведенные выше рассуждения правильными или я упускаю какие-либо веские причины, по которым логические объекты действительно нуждаются или как-то выигрывают от конструкторов копирования?

Автоматически сгенерированные конструкторы копирования являются большим преимуществом в отделении управления ресурсами от программной логики; классы, реализующие логику, вообще не должны беспокоиться о выделении, освобождении или копировании ресурсов.

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

Длинный ответ

При рассмотрении семантики копирования полезно разделить типы на четыре категории:

  • Примитивные типы с семантикой, определяемой языком;
  • Типы управления ресурсами (или RAII) с особыми требованиями;
  • Агрегированные типы, которые просто копируют каждого члена;
  • Полиморфные типы.

Примитивные типы — это то, чем они являются, поэтому они выходят за рамки вопроса; Я предполагаю, что радикального изменения языка, сломав десятилетия устаревшего кода, не произойдет. Полиморфные типы не могут быть скопированы (при сохранении динамического типа) без определяемых пользователем виртуальных функций или махинаций RTTI, поэтому они также выходят за рамки вопроса.

Таким образом, предложение заключается в следующем: необходимо, чтобы RAII и агрегатные типы реализовывали именованную функцию, а не конструктор копирования, если они должны быть скопированы.

Это не имеет большого значения для типов RAII; им просто нужно объявить функцию копирования с другим именем, а пользователи просто должны быть немного более многословными.

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

В вашем мире есть две возможности:

  • Либо язык знает о вашей функции копирования, и может автоматически сгенерировать ее (возможно, только при явном запросе, т.е. T copy() = default;, так как ты хочешь явности). По моему мнению, автоматическая генерация именованных функций на основе той же именованной функции в других типах больше напоминает магию, чем текущую схему генерации «языковых элементов» (конструкторов и перегрузок операторов), но, возможно, это всего лишь мой предрассудок.
  • Или пользователю остается правильно реализовать семантику копирования для агрегатов. Это подвержено ошибкам (поскольку вы можете добавить элемент и забыть обновить функцию), и нарушает текущее чистое разделение между управлением ресурсами и логикой программы.

И чтобы ответить на вопросы, которые вы делаете в пользу:

  1. Копирование (неполиморфных) объектов является обычное дело, хотя, как вы говорите, теперь это не так часто, когда их можно перемещать, когда это возможно. Это просто ваше мнение, что «явное лучше» или что T a(b); менее явный, чем T a(b.copy());
  2. Согласитесь, если объект не имеет четко определенной семантики копирования, то он должен иметь именованные функции, чтобы охватить любые предлагаемые им опции. Я не вижу, как это влияет на то, как нормальные объекты должны быть скопированы.
  3. Я понятия не имею, почему вы думаете, что конструктору копирования нельзя позволять делать то, что может делать именованная функция, если они являются частью определенной семантики копирования. Вы утверждаете, что конструкторы копирования не должны использоваться из-за искусственных ограничений, которые вы накладываете на них сами.
  4. Копирование полиморфных объектов — это совершенно другой котелок с рыбой. Принуждение всех типов к использованию именованных функций только потому, что полиморфные не должны давать согласованности, о которой вы, похоже, спорите, поскольку возвращаемые типы должны были бы отличаться. Полиморфные копии должны быть динамически размещены и возвращены указателем; неполиморфные копии должны быть возвращены по значению. На мой взгляд, нет никакой ценности в том, чтобы эти разные операции выглядели одинаково, не будучи взаимозаменяемыми.
2

Другие решения

Проблема в том, что означает слово «объект».

Если объекты являются ресурсами, на которые ссылаются переменные (как в java или в C ++ с помощью указателей, используя классические парадигмы ООП), каждая «копия между переменными» является «общим», и если навязывается единое владение, «общий доступ» становится «движущимся».

Если объекты сами переменные, так как каждая переменная должна иметь свою историю, вы не можете «двигаться», если не можете / не хотите навязывать уничтожение значения в пользу другого.

Косидер например std::strings:

   std::string a="Aa";
std::string b=a;
...
b = "Bb";

Ожидаете ли вы ценность a изменить или этот код не компилировать? Если нет, то копия нужна.

Теперь рассмотрим это:

   std::string a="Aa";
std::string b=std::move(a);
...
b = "Bb";

Теперь a оставлено пустым, так как его значение (лучше динамическая память, которая его содержит) было «перемещено» в b, Значение b затем изменится, и старый "Aa" отбрасываются.

По сути, перемещение работает, только если явно вызвано или если правильный аргумент является «временным», как в

  a = b+c;

где ресурс удерживается возвращением operator+ явно не требуется после назначения, следовательно, перемещение его в a, а не копировать в другое aЗанял место и удали его более эффективно.

Переместить и скопировать две разные вещи. Перемещение не является «заменой копии». Это более эффективный способ избежать копирования только во всех случаях, когда объект не требуется создать клон самого себя.

4

Один из примеров, когда конструкторы копий оказываются полезными, — это реализация строгое исключение гарантирует.

Чтобы проиллюстрировать это, давайте рассмотрим resize функция std::vector, Функция может быть реализована примерно следующим образом:

void std::vector::resize(std::size_t n)
{
if (n > capacity())
{
T *newData = new T [n];
for (std::size_t i = 0; i < capacity(); i++)
newData[i] = std::move(m_data[i]);
delete[] m_data;
m_data = newData;
}
else
{ /* ... */ }
}

Если resize функция должна была иметь строгую гарантию исключения, мы должны гарантировать, что в случае возникновения исключения состояние std::vector перед resize() звонок сохранен.

Если T не имеет конструктора перемещения, тогда мы будем использовать конструктор копирования по умолчанию. В этом случае, если конструктор копирования выдает исключение, мы все равно можем предоставить надежную гарантию исключения: мы просто delete newData массив и никакого вреда для std::vector было сделано.

Однако, если бы мы использовали конструктор перемещения T и это бросило исключение, тогда у нас есть куча Tы, которые были перемещены в newData массив. Откат этой операции не прост: если мы попытаемся переместить их обратно в m_data массив конструктор перемещения T может снова выкинуть исключение!

Для решения этой проблемы у нас есть std::move_if_noexcept функция. Эта функция будет использовать конструктор перемещения T если он помечен как noexceptв противном случае будет использован конструктор копирования. Это позволяет нам реализовать std::vector::resize таким образом, чтобы обеспечить сильную гарантию исключения.

Для полноты следует отметить, что C ++ 11 std::vector::resize не дает строгой гарантии исключения во всех случаях. Согласно www.cplusplus.com у нас есть следующие гарантии:

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

1

Вот вещь Перемещение — это новое значение по умолчанию — новое минимальное требование. Но копирование все еще часто является полезной и удобной операцией.

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

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

1
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector