Чтение онлайн

на главную

Жанры

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

Майерс Скотт

Шрифт:

std::string birthDate const;

std::string address const;

...

private:

std::string theName; // деталь реализации

Date theBirthDate; // деталь реализации

Address theAddress; // деталь реализации

};

Класс Person нельзя скомпилировать, не имея доступа к определению классов, с помощью которых он реализуется, а именно string, Date и Address. Такие определения обычно предоставляются посредством директивы #include, поэтому весьма вероятно, что в начале файла,

определяющего класс Person, вы найдете нечто вроде:

#include <string>

#include “date.h”

#include “address.h”

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

Можно задаться вопросом, почему C++ настаивает на размещении деталей реализации класса в определении класса. Например, почему нельзя определить Person следующим образом:

namespace std {

class string; // опережающее объявление

} // (некорректно – см. далее)

class Date; // опережающее объявление

class Address; // опережающее объявление

class Person {

public:

Person(const std::string& name, const Date& birthday,

const Address& addr);

std::string name const;

std::string birthDate const;

std::string address const;

...

};

Если бы такое было возможно, то пользователи класса Person должны были перекомпилировать свои программы только при изменении его интерфейса.

Увы, при реализации этой идеи мы наталкиваемся на две проблемы. Первая: string – это не класс, а typedef (синоним шаблона basic_string<char>). Поэтому опережающее объявление string некорректно. Правильное объявление гораздо сложнее, так как в нем участвуют дополнительные шаблоны. Впрочем, это не важно, потому что вы в любом случае не должны вручную объявлять какие-либо части стандартной библиотеки. Вместо этого просто включите с помощью #include правильные заголовки и успокойтесь. Стандартные заголовки вряд ли станут узким местом при компиляции, особенно если ваша среда разработки поддерживает предкомпилированные заголовочные файлы. Если на компиляцию стандартных заголовков все же уходит много времени, то может понадобиться изменить дизайн и избежать использования тех частей стандартной библиотеки, которые включать нежелательно.

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

int main

{

int x; // определяем int

Person p(params); // определяем Person

...

}

Когда компилятор видит определение x, он понимает, что должен выделить достаточно места (обычно в стеке) для размещения int. Нет проблем: каждый компилятор знает, какова длина int. Встречая определение p, компилятор учитывает,

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

Такой вопрос не возникает в языках типа SmallTalk или Java, потому что при определении объекта компиляторы выделяют только память, достаточную для хранения указателя на этот объект. Иначе говоря, эти языки интерпретируют вышеприведенный код, как если бы он был написан следующим образом:

int main

{

int x; // определяем int

Person *p; // определяем указатель на Person

...

}

Это вполне законная конструкция на C++, поэтому вы и сами сможете имитировать «сокрытие реализации объекта за указателем». В случае класса Person это можно сделать, например, разделив его на два класса: один – для представления интерфейса, а другой – для его реализации. Если класс, содержащий реализацию, назвать Personlmpl, то Person должен быть написан следующим образом:

#include <string> // компоненты стандартной библиотеки

// не могут быть объявлены предварительно

#include <memory> // для tr1::shared_ptr; см. далее

class PersonImpl; // опережающее объявление PersonImpl

class Date; // опережающее объявление классов,

class Address; // используемых в интерфейсе Person

class Person {

public:

Person(const std::string& name, const Date& birthday,

const Address& addr);

std::string name const;

std::string birthDate const;

std::string address const;

...

private: // указатель на реализацию:

std::tr1::shared_ptr<PersonImpl> pImpl; // см. в правиле 13 информацию

}; // о std::tr1::shared_ptr

Здесь главный класс (Person) не содержит никаких данных-членов, кроме указателя (в данном случае tr1::shared_ptr – см. правило 13) на свой класс реализации (Personlmpl). Такой дизайн часто называют «идиомой pimpl» («pointer to implementation» – указатель на реализацию). В подобных классах указатели часто называют pImpl, как в приведенном примере.

При таком дизайне пользователи класса Person не видят никаких деталей – дат, адресов и имен. Реализация может быть модифицирована как угодно, при этом перекомпилировать программы, в которых используется Person, не придется. Кроме того, поскольку пользователи не знают деталей реализации Person, они вряд ли напишут код, который каким-то образом будет зависеть от этих деталей. Вот это я и называю отделением интерфейса от реализации.

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

Поделиться:
Популярные книги

Замуж второй раз, или Ещё посмотрим, кто из нас попал!

Вудворт Франциска
Любовные романы:
любовно-фантастические романы
5.00
рейтинг книги
Замуж второй раз, или Ещё посмотрим, кто из нас попал!

Протокол "Наследник"

Лисина Александра
1. Гибрид
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Протокол Наследник

Сердце Дракона. Том 11

Клеванский Кирилл Сергеевич
11. Сердце дракона
Фантастика:
фэнтези
героическая фантастика
боевая фантастика
6.50
рейтинг книги
Сердце Дракона. Том 11

Вперед в прошлое!

Ратманов Денис
1. Вперед в прошлое
Фантастика:
попаданцы
5.00
рейтинг книги
Вперед в прошлое!

Шесть принцев для мисс Недотроги

Суббота Светлана
3. Мисс Недотрога
Фантастика:
фэнтези
7.92
рейтинг книги
Шесть принцев для мисс Недотроги

Хочу тебя любить

Тодорова Елена
Любовные романы:
современные любовные романы
5.67
рейтинг книги
Хочу тебя любить

Огни Аль-Тура. Желанная

Макушева Магда
3. Эйнар
Любовные романы:
любовно-фантастические романы
эро литература
5.25
рейтинг книги
Огни Аль-Тура. Желанная

Начальник милиции 2

Дамиров Рафаэль
2. Начальник милиции
Фантастика:
попаданцы
альтернативная история
5.00
рейтинг книги
Начальник милиции 2

Мастер...

Чащин Валерий
1. Мастер
Фантастика:
героическая фантастика
попаданцы
аниме
6.50
рейтинг книги
Мастер...

Сумеречный стрелок 8

Карелин Сергей Витальевич
8. Сумеречный стрелок
Фантастика:
городское фэнтези
попаданцы
альтернативная история
аниме
5.00
рейтинг книги
Сумеречный стрелок 8

Я до сих пор не князь. Книга XVI

Дрейк Сириус
16. Дорогой барон!
Фантастика:
юмористическое фэнтези
попаданцы
аниме
5.00
рейтинг книги
Я до сих пор не князь. Книга XVI

Обыкновенные ведьмы средней полосы

Шах Ольга
Любовные романы:
любовно-фантастические романы
5.00
рейтинг книги
Обыкновенные ведьмы средней полосы

Метаморфозы Катрин

Ром Полина
Фантастика:
фэнтези
8.26
рейтинг книги
Метаморфозы Катрин

Идеальный мир для Социопата

Сапфир Олег
1. Социопат
Фантастика:
боевая фантастика
рпг
постапокалипсис
6.17
рейтинг книги
Идеальный мир для Социопата