}; // см. правило 13
Независимо от того, где хранятся данные, с точки зрения «разбухания» кода важно лишь, что теперь многие (быть может, все) функции-члены SquareMatrix оказываются просто встроенными вызовами их версий из базового класса, которые теперь будут разделяются всеми матрицами, содержащими данные одного и того же типа, независимо от их размера. В то же время объекты SquareMatrix разных размеров относятся к разным типам. Поэтому, несмотря на то что классы SquareMatrix<double, 5> и SquareMatrix<double, 10> пользуются одними и теми же функциями, определенными в SquareMatrixBase<double>, не получится передать функции, ожидающей параметра типа SquareMatrix<double, 10>, объект типа SquareMatrix<double, 5>. Хорошо, не правда ли?
Да, хорошо, но не бесплатно. Для функции invert с жестко «зашитой» в исходный текст размерностью матрицы, скорее всего, был бы сгенерирован более эффективный код, чем разделяемой функции, которой размерность передается в качестве параметра либо хранится в самом объекте. Например, «зашитая» размерность может быть константой времени компиляции, так что к ней будут применимы различные виды оптимизации, в частности встраивание константы непосредственно в машинную команду в виде непосредственного операнда. Для функции, не зависящей от размерности, такой номер не пройдет.
С другой стороны, наличие только одной версии invert для разных размерностей уменьшает объем исполняемого кода, а это, в свою очередь, уменьшит размер рабочего множества программы и улучшит локальность ссылок в кэше команд. Это может ускорить исполнение программы настолько, что все потери эффективности по сравнению с зависящей от размерности версией будут с лихвой компенсированы. Какой эффект окажется доминирующим? Единственный способ получить ответ – попробовать оба варианта и исследовать поведение на вашей конкретной платформе с репрезентативными наборами данных.
Другой фактор, влияющий на эффективность, – это размеры объектов. Если вы не будете внимательны, то перенос независимых от размерности функций в базовый класс может привести к увеличению размера каждого объекта. Например, в только что приведенном коде для каждого объекта SquareMatrix имеется указатель на его данные в классе SquareMatrixBase, несмотря даже на то, что производный класс и так может получить эти данные. Это увеличивает размер каждого объекта SquareMatrix, по крайней мере, на размер указателя. Можно модифицировать класс так, чтобы необходимость в этих указателях отпала, но это компромисс. Например, если завести в базовом классе защищенный член для хранения указателя на данные матрицы, то мы пожертвуем инкапсуляцией (см. правило 22). Это также может привести к усложнению алгоритмов управления ресурсами. Если в базовом классе хранится указатель на данные матрицы, то память для этих данных может быть либо выделена динамически, либо физически находиться внутри объекта производного класса (как мы видели). Так как же базовый класс определит, следует ли удалять указатель? Ответы на такие вопросы существуют, но чем изощреннее ваш дизайн, тем сложнее все получается. В некоторый момент умеренное дублирование кода может даже показаться спасением.
В этом правиле мы обсуждаем только разбухание кода из-за параметров шаблонов, не являющихся типами, но и параметры-типы могут привести к тому же. Например, на многих платформах int и long имеют одно и то же двоичное представление, поэтому функции-члены, скажем, для vector<int> и vector<long>, могут оказаться идентичными – разбухание в чистом виде. Некоторые компоновщики объединяют идентичные реализации функций, а некоторые – нет, и, значит, некоторые шаблоны, конкретизированные для int и для long, на одних платформах приводят к разбуханию, а на других – нет. Аналогично на большинстве платформ все типы указателей имеют одинаковое двоичное представление, поэтому шаблоны с параметрами указательных типов (например, list<int*>, list<const int*>, list<SquareMatrix<long,3>*> и т. п.) зачастую могли бы использовать общие реализации всех функций-членов. Как правило, это означает, что функции-члены, которые работают со строго типизованными указателями (например, T*) должны внутри себя вызывать функции, работающие с нетипизированными указателями (то есть void*). В некоторых реализациях стандартной библиотеки C++ такой подход применен к шаблонам vector, deque и list и им подобным. Если вас беспокоит опасность разбухания кода из-за использования шаблонов, возможно, стоит поступить аналогично.
• Шаблоны генерируют множество классов и функций, поэтому любой встречающийся в шаблоне код, который не зависит от параметров шаблона, приводит к разбуханию кода.
• Разбухания из-за параметров шаблонов, не являющихся типами, часто можно избежать, заменив параметры шаблонов параметрами функций или данными-членами класса.
• Разбухание из-за параметров-типов можно ограничить, обеспечив общие реализации для случаев, когда шаблон конкретизируется типами с одинаковым двоичным представлением.
Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»
Для чего обычные указатели хороши – так это для поддержки неявных преобразований типов. Указатели на объекты производных классов неявно преобразуются в указатели на объекты базовых классов, указатели на неконстантные объекты – в указатели на константные и т. п. Например, рассмотрим некоторые преобразования, которые могут происходить в трехуровневой иерархии:
class Top {...};
class Middle: public Top {...};
class Bottom: public Middle {...};
Top *pt1 = new Middle; // преобразует Middle* в Top*
Top *pt2 = new Bottom; // преобразует Middle* в Bottom*
Const Top *pct2 = pt1; // преобразует Top* в const Top*
Эмулировать такие преобразования с помощью определяемых пользователем «интеллектуальных» указателей не просто. Для этого нужно, чтобы компилировался такой код:
Template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T *realPtr); // интеллектуальные указатели обычно
... // инициализируются встроенными
}; // указателями
SmartPtr<Top> pt1 = // преобразует SmartPtr<Middle>
SmartPtr<Middle>(new Middle); // в SmartPtr<Top>
SmartPtr<Top> pt2 = // преобразует SmartPtr<Bottom>
SmartPtr<Bottom>(new Bottom); // SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1;
Разные конкретизации одного шаблона не связаны каким-либо отношением, поэтому компилятор считает, что SmartPtr<Middle> и SmartPtr<Top> – совершенно разные классы, не более связанные друг с другом, чем, например, vector<float> и Widget. Чтобы можно было осуществлять преобразования между разными классами SmartPtr, необходимо явно написать соответствующий код. В приведенном выше примере каждое предложение создает новый объект интеллектуального указателя, поэтому для начала сосредоточимся на написании конструкторов, которые будут вести себя так, как нам нужно. Ключевое наблюдение состоит в том, что невозможно написать сразу все необходимые конструкторы. В приведенной иерархии мы можем сконструировать SmartPtr<Top> из SmartPtr<Middle> или SmartPtr<Bottom>, но если в будущем иерархия будет расширена, то придется добавить возможность конструирования объектов SmartPtr<Top> из других типов интеллектуальных указателей. Например, если мы позже добавим такой класс:
class BelowBottom: public Bottom {...};