Обучающие курсы:

Обучение профессии "Разработчик C#" + стажировка в Mail.ru
Обучение профессии "Разработчик Python" + трудоустройство
Обучение профессии "Веб-разработчик" + стажировка в Mail.ru


Главная страница
Библиотека (скачать книги)
Скачать софт
Введение в программирование
Стандарты для C++
Уроки по C#
Уроки по Python
HTML
Веб-дизайн
Ассемблер в среде Windows
ActiveX
Javascript
Общее о Линукс
Линукс - подробно
Линукс - новое
Delphi
Паскаль для начинающих
Турбопаскаль
Новости
Партнеры
Наши предложения
Архив новостей







52. Копируйте и ликвидируйте согласованно

Резюме

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

Обсуждение

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

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

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

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

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

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

Исключения

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

В редких случаях классы, имеющие члены странных типов (например, ссылки, std::auto_ptr), являются исключениями, поскольку они имеют необычную семантику копирования. В классе, хранящем ссылку или auto_ptr, вам, вероятно, потребуется написать копирующий конструктор и оператор присваивания, но деструктор по умолчанию будет работать корректно. (Заметим, что использование члена, являющегося ссылкой или auto_ptr, почти всегда ошибочно).

53. Явно разрешайте или запрещайте копирование

Резюме

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

Обсуждение

Распространенной ошибкой (и не только среди новичков) является игнорирование семантики копирования и присваивания при определении класса. Это характерно для маленьких вспомогательных классов, таких как предназначенные для поддержки идиомы RAII (см. рекомендацию 13).

Убедитесь, что ваш класс предоставляет осмысленное копирование (или не предоставляет его вовсе). Вот возможные варианты.

  • Явное запрещение обеих функций. Если копирование для вашего типа лишено смысла, запретите как копирующее конструирование, так и копирующее присваивание, объявив их как закрытые нереализованные функции:

    class T { //...
    private:                      // Делаем Т некопируемым
        Т( const T& );            // Функция не реализована
        Т& operator=( const T& ); // Функция не реализована
    };
    
  • Явное написание обеих функций. Если для объектов Т предусмотрены копирование и копирующее присваивание, но корректное копирующее поведение отличается от поведения сгенерированных компилятором версий, то следует явно написать обе функции и сделать их не закрытыми.

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

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

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

54. Избегайте срезки. Подумайте об использовании в базовом классе клонирования вместо копирования

Резюме

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

Обсуждение

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

class B { /* ... */ };

class D : public в { /* ... */ };

// Оп! Получение объекта по значению
void Transmogrify( B obj );

void Transubstantiate( B& obj ) { // все нормально -
                                  // передача по ссылке
    Transmogrify( obj );          // Плохо! Срезка объекта!
}

D d;
Transubstantiate( d );

Программист намерен работать с объектами B и производных классов полиморфно. Однако, по ошибке (или усталости — к тому же и кофе закончился...) программист или просто забыл написать & в сигнатуре Transmogrify, или собирался создать копию, но сделал это неверно. Код компилируется без ошибок, но когда функция Transmogrify вызывается с передачей ей объекта D, он мутирует в объект В. Это связано с тем, что передача по значению приводит к вызову В::В(const B&), т.е. копирующего конструктора В, параметр которого const B& представляет собой автоматически преобразованную ссылку на d. Что приводит к полной потере динамического, полиморфного поведения, из-за которого в первую очередь и используется наследование.

Если, как автор класса В, вы хотите разрешить срезку, но не хотите, чтобы она могла происходить по ошибке, для такого случая существует один вариант действий, о котором мы упомянем для полноты изложения, но не рекомендуем использовать его в коде, к которому предъявляется требование переносимости: вы можете объявить копирующий конструктор В как explicit. Это может помочь избежать неявной срезки, но кроме этого запрещает все передачи параметров данного типа по значению (что может оказаться вполне приемлемым для базовых классов, объекты которых все равно не должны создаваться; см. рекомендацию 35).

// объявляем копирующий конструктор как explicit (у данного
// решения имеется побочное действие, так что требуется
// улучшение этого метода)
class B { // ...
public:
    explicit B( const B& rhs );
};

class D : public B { /* ... */ };

Вызывающий код все равно в состоянии выполнить срезку, если это необходимо, но должен делать это явно:

void Transmogrify( B obj );         // Теперь эта функция вообще не
                                    // может быть вызвана (!)
void Transmogrify2( const B& obj )  // Идиома для намерения в
{                                   // любом случае получить
    В b( obj );                     // параметр obj  по значению
    // ...                          // (с возможной срезкой)
}

B b;              // Базовые классы не должны быть конкретными
D d;              // (см. рекомендацию 35), но допустим это
Transmogrify(b);  // Должна быть ошибка (см. примечание)
Transmogrify(d);  // Должна быть ошибка (см. примечание)
Transmogrify2(d); // Все в порядке

Примечание: на момент написания данной рекомендации некоторые компиляторы ошибочно допускали один или оба приведенных вызова функции Transmogrify. Эта идиома вполне стандартна, но (пока что) не полностью переносима.

Имеется лучший способ предупреждения срезки, с более высокой степенью переносимости. Пусть, например, функция наподобие Transmogrify в действительности хочет получить полную глубокую копию без информации о действительном производном типе переданного объекта. Более общее идиоматическое решение состоит в том, чтобы сделать копирующий конструктор базового класса защищенным (чтобы функция наподобие Transmogrify не могла случайно его вызвать), а вместо него воспользоваться виртуальной функцией Clone:

// добавление функции Clone (уже лучше, но все еще требуется
// усовершенствование)
class B { // ...
public:
    virtual B* Clone() const = 0;
protected:
    B( const B& );
};

class D : public B { // ...
public:
    virtual D* Clone () const { return new D(*this); }
protected:
    D( const D& rhs ) : B( rhs ) { /*...*/ }
};

Теперь попытка срезки будет (переносимо) генерировать ошибку времени компиляции, а объявление функции Clone как чисто виртуальной заставляет непосредственный производный класс перекрыть ее. К сожалению, с данным решением все еще связаны две проблемы, которые компилятор не в состоянии обнаружить: в классе, производном от производного, функция Clone может оказаться неперекрытой, а перекрытие Clone может реализовать ее некорректно, так что копия будет не того же типа, что и оригинал. Функция Clone должна следовать шаблону проектирования Nonvirtual Interface (NVI; см. рекомендацию 39), который разделяет открытую и виртуальную природы Cl one и позволяет вам использовать ряд важных проверок:

class В { // ...
public:
    B* clone() const { // невиртуальная функция
    B* p = DoClone();
    assert(typeid(*p) == typeid(*this) &&
           "DoClone incorrectly overridden");
    return p;          // проверка типа, возвращаемого DoClone
protected:
    B( const B& );
private:
    virtual B* DoClone() const = 0;
};

Функция Clone теперь является невиртуальным интерфейсом, используемым вызывающим кодом. Производные классы должны перекрыть функцию DoClone. Дополнительная проверка обнаружит все копии, которые имеют тип, отличный от оригинала, тем самым оповещая, что в некотором производном классе не перекрыта функция DoClone; в конце концов, задача assert состоит именно в обнаружении и сообщении о таких программных ошибках (см. рекомендации 68 и 70).

Исключения

Некоторые проектные решения могут требовать, чтобы копирующие конструкторы базовых классов оставались открытыми (например, когда часть вашей иерархии представляет собой библиотеку стороннего производителя). В таком случае следует предпочесть передачу посредством (интеллектуального) указателя передаче по ссылке; как показано в рекомендации 25, передача посредством указателя существенно менее подвержена срезке и нежелательному созданию временных объектов.

55. Предпочитайте канонический вид присваивания

Резюме

При реализации оператора operator= предпочитайте использовать канонический вид — невиртуальный с определенной сигнатурой.

Обсуждение

Предпочтительно объявлять копирующее присваивание для типа Т с одной из следующих сигнатур:

T& operator=( const T& );  // Классический вид
T& operator=( T )          // Потенциально оптимизированный
                           // вид (см. рекомендацию 27)

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

Избегайте делать любой оператор присваивания виртуальным. Если вы полагаете, что вам требуется виртуальное поведение присваивания, обратитесь сначала к указанной литературе. Если и после этого вы стоите на своем, то лучше использовать виртуальную именованную функцию, а не оператор (например, virtual void Assign(const T&);).

He возвращайте const T&. Хотя этот тип возвращаемого значения имеет то преимущество, что защищает от странных присваиваний наподобие (а=b)=c, главным его недостатком является то, что вы не сможете поместить объекты типа Т в контейнеры стандартной библиотеки; эти контейнеры требуют, чтобы оператор присваивания возвращал тип Т&.

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

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

Явно вызывайте все операторы присваивания базовых классов и всех данных-членов; обратите внимание, что идиома обмена автоматически заботится обо всех этих вещах. Возвращайте из оператора присваивания значение *this.

56. Обеспечьте бессбойную функцию обмена

Резюме

Обычно имеет смысл предоставить для класса функцию swap в целях эффективного и бессбойного обмена внутреннего содержимого объекта с внутренним содержимым другого объекта. Такая функция может пригодиться для реализации ряда идиом, от простого перемещения объектов до реализации присваивания, легко обеспечивающего функцию принятия результатов работы со строгими гарантиями безопасности для вызывающего кода (см. также рекомендацию 51).

Обсуждение

Обычно функция swap выглядит примерно следующим образом (здесь U — некоторый пользовательский тип):

class Т { // ...
public:
    void swap( T& rhs ) {
        member1_.swap( rhs.member1_ );
        std::swap( member2_, rhs.member2_ );
    }
private:
    U member1_;
    int member2_;
};

Для примитивных типов и стандартных контейнеров можно использовать std::swap. Другие классы могут реализовывать обмен в виде функций-членов с различными именами.

Рассмотрим использование swap для реализации копирующего присваивания посредством копирующего конструктора. Приведенная далее реализация оператора operators обеспечивает строгую гарантию (см. рекомендацию 71), хотя и ценой создания дополнительного объекта, что может оказаться неприемлемым, если имеется более эффективный способ выполнения безопасного присваивания объектов типа Т:

T& T::operator=(const T& other) { // вариант 1 (традиционный)
    T temp( other );
    swap( temp );
    return *this;
}

T& T::operator=(T temp) { // вариант 2 (см. рекомендацию 27)
    swap( temp );         // обратите внимание на передачу
    return *tnis;         // temp по значению
}

Но что если тип U не имеет бессбойной функции обмена, как в случае многих существующих классов, но вам требуется поддержка функции обмена для типа Т? Не все потеряно.

  • Если копирующий конструктор и оператор копирующего присваивания U не дают сбоев, то с объектами типа U вполне справится std::swap.

  • Если копирующий конструктор U может давать сбой, вы можете хранить (интеллектуальный) указатель на U вместо непосредственного члена. Указатели легко обмениваются. Следствием их применения являются дополнительные расходы на одно динамическое выделение памяти и дополнительную косвенность при обращении, но если вы храните все такие члены в едином Pimpl-объекте, то для всех закрытых членов дополнительные расходы вы понесете только один раз (см. рекомендацию 43).

Никогда не пользуйтесь трюком реализации копирующего присваивания посредством копирующего конструирования с использованием непосредственного вызова деструктора и размещающего new, несмотря на то, что такой трюк регулярно "всплывает" в форумах, посвященных C++ (см. также рекомендацию 99). Так что никогда не пишите:

T& T::operator=( const T& rhs ) { // Плохо: анти-идиома
    if( this != &rhs ) {
        this->~T();               // Плохая методика!
        new (this) T( rhs );      // 
    }
    return *this;
}

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

namespace std {
    template<> void swap( MyType& lhs, MyType& rhs) {
        lhs.swap( rhs );  // Для объектов MyType используется
    }                     // MyType::swap
}

Стандарт не позволяет вам сделать это, если МуТуре сам является шаблонным классом. К счастью, иметь такую специализацию хорошо, но не обязательно; основная методика состоит в обеспечении функции swap, эффективно работающей с данным типом, в виде функции, не являющейся членом класса, в том же пространстве имен, в котором находится и ваш тип.

Исключения

Обмен важен для классов с семантикой значения. Существенно менее важна она для базовых классов, поскольку эти классы в любом случае используются посредством указателей (см. рекомендации 32 и 54).



 
 

Библиотека программиста. 2009.
Администратор: admin@programmer-lib.ru