Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 8 9 10 11 12 13 14 15 16 ... 73
Перейти на страницу:

NamedObject<int>no1(“Smallest Prime Number”, 2);

NamedObject<int>no2(no1); // вызывается конструктор копирования

Конструктор копирования, сгенерированный компилятором, должен инициализировать no2.nameValue и no2.objectValue, используя nol.nameValue и nol.objectValue соответственно. Член nameValue имеет тип string, а в стандартном классе string объявлен конструктор копирования, поэтому no2. nameValue будет инициализирован вызовом конструктора копирования string с аргументов nol.nameValue. С другой стороны, член NameObject<int>::objectValue имеет тип int (поскольку T есть int в данной конкретизации шаблона), а int – встроенный тип, поэтому no2.objectValue будет инициализирован побитовым копированием nol.objectValue.

Сгенерированный компилятором оператор присваивания для класса Named-Object<int> будет вести себя аналогичным образом, но, вообще говоря, сгенерированная компилятором версия оператора присваивания ведет себя так, как я описал, только в том случае, когда в результате получается корректный и осмысленный код. В противном случае компилятор не сгенерирует operator=.

Например, предположим, что класс NamedObject определен, как показано ниже. Обратите внимание, что nameValue – ссылка на string, а objectValue имеет тип const T:

template<class T>

class NamedObject {

public:

// этот конструктор более не принимает const name, поскольку nameValue –

// теперь ссылка на неконстантную строку. Конструктор с аргументом типа

// char* исключен, поскольку нам нужна строка, на которую можно сослаться

NamedObject(std::string& name, const T& value);

... // как и ранее, предполагаем,

// что operator= не объявлен

private:

std::string& nameValue; // теперь это ссылка

const T objectValue; // теперь const

};

Посмотрим, что произойдет в приведенном ниже коде:

std::string newDog(“Persephone”);

std::string oldDog(“Satch”);

NamedObject<int> p(newDog, 2); // Когда я впервые написал это,

// наша собака Персефона собиралась

// встретить свой второй день рождения

NamedObject<int> s(oldDog, 36); // Семейному псу Сатчу (из моего

// детства) было бы теперь 36 лет

p = s; // Что должно произойти

// с данными-членами p?

Перед присваиванием и p.nameValue, и s.nameValue ссылались на объекты string, хотя и на разные. Что должно произойти с членом p.nameValue в результате присваивания? Должен ли он ссылаться на ту же строку, что и s.nameValue, то есть должна ли модифицироваться ссылка? Если да, это подрывает основы, потому что C++ не позволяет изменить объект, на который указывает ссылка. Но, быть может, должна модифицироваться строка, на которую ссылается член p.nameValue, и тогда будут затронуты другие объекты, содержащие указатели или ссылки на эту строку, хотя они и не участвовали непосредственно в присваивании? Это ли должен делать сгенерированный компилятором оператор присваивания?

Сталкиваясь с подобной головоломкой, C++ просто отказывается компилировать этот код. Если вы хотите поддерживать присваивание в классе, включающем в себя член-ссылку, то должны определить оператор присваивания самостоятельно. Аналогичным образом компилятор ведет себя с классами, содержащими константные члены (такие как objectValue во втором варианте класса NamedObject выше). Модифицировать константные члены запрещено, поэтому компилятор не знает, как поступать при неявной генерации оператора присваивания. Кроме того, компилятор не станет неявно генерировать оператор присваивания в производном классе, если в его базовом объявлен закрытый оператор присваивания. И наконец, предполагается, что сгенерированные компилятором операторы присваивания для производных классов должны обрабатывать части базовых классов (см. правило 12), но при этом они конечно же не могут вызывать функции-члены, доступ к которым для них запрещен.

Что следует помнить

• Компилятор может неявно генерировать для класса конструктор по умолчанию, конструктор копирования, оператор присваивания и деструктор.

Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны

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

class HomeForSale {...};

Любой агент по продаже недвижимости скажет вам, что каждый объект уникален – не бывает двух, в точности одинаковых. Вот почему идея создания копии объекта HomeForSale бессмысленна. Как можно скопировать нечто, по определению, уникальное? Поэтому хотелось бы, чтобы попытки скопировать объекты HomeForSale не компилировались:

HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1); // попытка скопировать h1 –

// не должно компилироваться!

h1 = h2; // попытка скопировать h2 –

// не должно компилироваться!

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

Похоже на безвыходное положение. Если вы сами не объявите конструктор копирования или оператор присваивания, то их сгенерирует компилятор. И ваш класс будет поддерживать копирование. Но то же самое произойдет, если вы объявите эти функции самостоятельно. Однако наша цель – предотвратить копирование!

Ключ к решению в том, что все сгенерированные компилятором функции являются открытыми. Чтобы предотвратить автоматическое генерирование, вы должны объявить их самостоятельно, но никто не требует, чтобы они были открытыми. Ну так и объявите конструктор копирования и оператор присваивания закрытыми. Объявляя явно функцию-член, вы предотвращаете генерирование ее компилятором, а сделав ее закрытой, не позволяете кому-либо вызывать ее.

Схема не идеальна, потому что другие члены класса и функции-друзья по-прежнему могут вызывать закрытые функции. Если только вы не включите лишь объявление, опустив определение. Тогда если кто-то случайно вызовет такую функцию, то получит сообщение об ошибке на этапе компоновки. Этот трюк – объявление функций-членов закрытыми и сознательный отказ от их реализации – как раз и используется для предотвращения копирования в некоторых классах библиотеки iostreams. Взгляните, например, на объявления классов ios_base, basic_ios и sentry в вашей реализации стандартной библиотеки. Вы обнаружите, что в каждом случае как конструктор копирования, так и оператор присваивания объявлены закрытыми и нигде не определены.

Применить эту уловку в классе HomeForSale несложно:

class HomeForSale {

public:

...

private:

HomeForSale(const HomeForSale&); // только объявления

HomeForSale& oparetor=( const HomeForSale&);

};

Заметьте, что я не указал имена параметров функций. Это необязательно, просто таково общее соглашение. Ведь раз эти функции никогда не будут реализовываться и использоваться, то какой смысл задавать имена их параметров?

При таком определении компилятор будет блокировать любые попытки клиентов копировать объекты HomeForSale, а если вы случайно попытаетесь сделать это в функции-члене или функции-друге класса, то об ошибке сообщит компоновщик.

Существует возможность переместить ошибку с этапа компоновки на этап компиляции (это всегда полезно – лучше обнаружить ошибку как можно раньше), если объявить конструктор копирования и оператор присваивания закрытыми не в самом классе HomeForSale, а в его базовом классе, специально созданном для предотвращения копирования. Такой базовый класс очень прост:

class Uncopyable {

protected:

Uncopyable() {} // разрешить конструирование

~Uncopyable() {} // и уничтожение

// объектов производных классов

private:

Uncopyable(const Uncopyable&); // но предотвратить копирование

Uncopyable& operator=(const Uncopyable&);

};

Чтобы предотвратить копирование объектов HomeForSale, нужно лишь унаследовать его от Uncopyable:

class HomeForSale : private Uncopyable { // в этом класс больше нет ни

... // конструктора копирования, ни

} // оператора присваивания

Такое решение работает, потому что компилятор пытается генерировать конструктор копирования и оператор присваивания, если где-то – пусть даже в функции-члене или дружественной функции – производится попытка скопировать объект HomeForSale. Как объясняется в правиле 12, сгенерированные компилятором версии будут вызывать соответствующие функции из базового класса. Но это не получится, так как в базовом классе они объявлены закрытыми.

Реализация и использование класса Uncopyable сопряжена с некоторыми тонкостями. Например, наследование от Uncopyable не должно быть открытым (см. правила 32 и 39), а деструктор Uncopyable не должен быть виртуальным (см. правило 7). Поскольку Uncopyable не имеет данных-членов, то компилятор может прибегнуть к оптимизации пустых базовых классов, описанной в правиле 39, но коль скоро этот класс базовый, то возможно возникновение множественного наследования (см. правило 40). А множественное наследование в некоторых случаях не дает возможности провести оптимизацию пустых базовых классов (см. правило 39). Вообще говоря, вы можете игнорировать эти тонкости и просто использовать Uncopyable, как показано выше. Можете также воспользоваться версией из билиотеки Boost (см. правило 55). В ней этот класс называется noncopyable. Это хороший класс, но мне просто показалось, что его название немного, скажем так, неестественное.

1 ... 8 9 10 11 12 13 14 15 16 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии