2013-05-08 3 views
2

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

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

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

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

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

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

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

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


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

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

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

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

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

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

+3

«Копирование объектов на самом деле не является обычным явлением». - Я бы сказал, что это довольно распространено, учитывая, что C++ по умолчанию использует семантику значений. Два объекта с одинаковым значением должны представлять одно и то же. –

+3

Отключение копирования по умолчанию может иметь некоторые достоинства; но почему бы вам включить его через функцию-член, а не идиоматическое использование конструкторов копирования? –

+0

@sftrabbit: необязательно: значения не являются полиморфными, ссылки и указатели. Если вы хотите, чтобы полиморфизм во время выполнения на C++ идентифицировал объекты по их адресам, а не значения (которые представляют собой только «состояние», а не «идентификатор»). Два 'Person'-s, имеющие одно и то же' m_name', не совпадают с 'Person'. –

ответ

2

Short Anwer

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

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

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

Длинного ответ

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

  • Примитивных типов, с семантикой, определенной в языке;
  • Виды управления ресурсами (или RAII) со специальными требованиями;
  • Совокупные типы, которые просто копируют каждый элемент;
  • Полиморфные типы.

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

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

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

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

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

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

И решать вопросы, которые вы делаете в пользу:

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

Спасибо, Майк! Я ценю автокопирование ctors как помощь при написании тривиальных или нетривиальных подпрограмм копирования. Мое предложение состояло в том, чтобы: 1) оставить пользовательские копии ctors для типов RAII; 2) оставить общедоступные копии ctors по умолчанию для агрегатов; 3) предоставить копию() или виртуальную копию() вместо общедоступной копии ctor везде. Одна вещь, которую я упустил, - это требование, чтобы полиморфные копии полагались на динамически распределенную память. Теперь я вижу, что предоставление названных методов копирования все равно не сможет унифицировать копирование интерфейса между полиморфными и неполиморфными неагрегатами (или «логическими объектами», как я уже говорил ранее). – Kos

+0

Предложение, которое я по-прежнему поддерживаю, - это использовать именованный метод, если логика программы требует, чтобы объект был скопирован нетривиальным способом. В таком случае я бы все же настаивал на том, чтобы иметь именованный метод и никакой публичный экземпляр ctor. Зачем? Легко сделать ошибку и использовать копию ctor вместо перемещения ctor, когда единственным намерением является перераспределение, а не дублирование, связанное с логикой. Это может быть так же просто, как исключить один «noexcept». Копировальные конструкторы по-прежнему сложны из-за их двойной роли (связанной с логикой и связанной с перераспределением) - этот факт привел меня ко всему этому. – Kos

+0

Это действительно интересное продолжение - это * любая * логическая копия неагрегата, фактически «тривиальная»? Возможно нет; Мне нужно переварить это. Я хотел бы вернуться к вам, если придумаю убедительный пример. – Kos

4

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

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

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

Cosider, например 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"; 

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

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

a = b+c; 

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

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

+0

Спасибо! Объекты, безусловно, отличаются от переменных, но C++ дает редкую (и полезную) опцию для значения * переменной * объекта - объединить хранилище объекта с переменной. В вашем примере показаны две переменные типа значения, что означает, что должны быть 2 реальных, отдельных объекта, возможно, копия другой. И да, я бы предпочел, чтобы команда 'string b = a' завершилась неудачей во время компиляции и' string b = a.copy() 'для успеха. – Kos

+0

@ Kos: Вы действительно можете это сделать. Но не то, что C++ навязывает вам делать. Если вам нужна инфраструктура, где все объекты находятся в куче, и только эталонные обертки/умные указатели находятся в стеке, вы можете это сделать. Но это не так сильно отличается от java. Почему бы просто не использовать Java? Не отвечайте. Просто подумайте и решите. Контекст - в этом случае - имеет значение больше всего. Но с точки зрения языка эти «ссылочные обертки» или «умный указатель» сами являются ... классами значений. Таким образом, язык не может обойтись без него! –

+0

@Kos: от идиоматической точки зрения, 'strring b = copy (a)' должно быть, лучше, с парой с строкой b = move (a) '. Скопируйте и передвиньте роль «манипуляторов», а не «членов». –

1

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

Чтобы проиллюстрировать это, рассмотрим функцию resizestd::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 больше, и происходит перераспределение, в случае исключения в контейнере нет изменений (сильная гарантия), если тип элементов является либо скопированным, либо невозвратным. В противном случае, если выбрано исключение, контейнер остается с допустимым состоянием (основная гарантия).

+0

Спасибо, это важный момент, который вы подняли здесь. Но действительно ли это нормально (или: всегда ли правильно) бросать 'swap', перемещать ctors и перемещать op = s? Я даже видел [предложение] (http: //www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2855.html # nothrowmove), чтобы сделать notchrow обязательным здесь. – Kos

+0

@ Kos: К сожалению, он находится в текущем состоянии C++. – Puppy

1

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

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

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