Соображения эффективности A
В общем случае объект класса эффективнее передавать функции по указателю или по ссылке, нежели по значению. Например, если дана функция с сигнатурой:
bool sufficient_funds( Account acct, double );
то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct
значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур:
bool sufficient_funds( Account *pacct, double ); |
bool sufficient_funds( Account &acct, double );
то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).
Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения:
// задача решается, но для больших матриц эффективность может // оказаться неприемлемо низкой Matrix operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить арифметические операции ... return result; |
}
Этот перегруженный оператор позволяет пользователю писать
Matrix a, b; // ... // в обоих случаях вызывается operator+() Matrix c = a + b; |
a = b + c;
Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix
представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.
Следующая пересмотренная реализация намного увеличивает скорость:
// более эффективно, но после возврата адрес оказывается недействительным // это может привести к краху программы Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить сложение ... return result; |
}
но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)
Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается:
// нет возможности гарантировать отсутствие утечки памяти // поскольку матрица может быть большой, утечки будут весьма заметными Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix *result = new Matrix; // выполнить сложение ... return *result; |
Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей программы не отвечает за применение оператора delete к объекту по окончании его использования.
Вместо оператора сложения лучше применять именованную функцию, которой в качестве третьего параметра передается ссылка, где следует сохранить результат:
// это обеспечивает нужную эффективность, // но не является интуитивно понятным для пользователя void mat_add( Matrix &result, const Matrix& m1, const Matrix& m3 ) { // вычислить результат |
Таким образом, проблема производительности решается, но для класса уже нельзя использовать операторный синтаксис, так что теряется возможность инициализировать объекты
// более не поддерживается |
и использовать их в выражениях:
// тоже не поддерживается |
Неэффективный возврат объекта класса – слабое место С++. В качестве одного из решений предлагалось расширить язык, введя имя возвращаемого функцией объекта:
Matrix& operator+( const Matrix& m1, const Matrix& m2 ) name result { Matrix result; // ... return result; |
Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку:
// переписанная компилятором функция // в случае принятия предлагавшегося расширения языка void operator+( Matrix &result, const Matrix& m1, const Matrix& m2 ) name result { // вычислить результат |
и преобразовать все вызовы этой функции, разместив результат непосредственно в области, на которую ссылается первый параметр. Например:
Matrix c = a + b;
было бы трансформировано в
Matrix c; |
Это расширение так и не стало частью языка, но предложенная оптимизация прижилась. Компилятор в состоянии распознать, что возвращается объект класса и выполнить трансформацию его значения и без явного расширения языка. Если дана функция общего вида:
classType functionName( paramList ) { classType namedResult; // выполнить какие-то действия ... return namedResult; |
то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней:
void functionName( classType &namedResult, paramList ) { // вычислить результат и разместить его по адресу namedResult |
что позволяет уйти от необходимости возвращать значение объекта и вызывать копирующий конструктор. Чтобы такая оптимизация была применена, в каждой точке возврата из функции должен возвращаться один и тот же именованный объект класса.
И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида
Matrix c = a + b;
всегда эффективнее присваивания. Например, результат следующих двух инструкций такой же, как и в предыдущем случае:
Matrix c; |
но объем требуемых вычислений значительно больше. Аналогично эффективнее писать:
for ( int ix = 0; ix < size-2; ++ix ) { Matrix matSum = mat[ix] + mat[ix+1]; // ... |
чем
Matrix matSum; for ( int ix = 0; ix < size-2; ++ix ) { matSum = mat[ix] + mat[ix+1]; // ... |
Причина, по которой присваивание всегда менее эффективно, состоит в том, что возвращенный локальный объект нельзя подставить вместо объекта в левой части оператора присваивания. Иными словами, в то время как инструкцию
Point3d p3 = operator+( p1, p2 );
можно безопасно трансформировать:
// Псевдокод на C++ Point3d p3; |
преобразование
Point3d p3; |
в
// Псевдокод на C++ // небезопасно в случае присваивания |
небезопасно.
Преобразованная функция требует, чтобы переданный ей объект представлял собой неформатированную область памяти. Почему? Потому что к объекту сразу применяется конструктор, который уже был применен к именованному локальному объекту. Если переданный объект уже был сконструирован, то делать это еще раз с семантической точки зрения неверно.
Что касается инициализируемого объекта, то отведенная под него память еще не подвергалась обработке. Если же объекту присваивается значение и в классе объявлены конструкторы (а именно этот случай мы и рассматриваем), можно утверждать, что эта память уже форматировалась одним из них, так что непосредственно передавать объект функции небезопасно.
Вместо этого компилятор должен создать неформатированную область памяти в виде временного объекта класса, передать его функции, а затем почленно присвоить возвращенный временный объект объекту, стоящему в левой части оператора присваивания. Наконец, если у класса есть деструктор, то он применяется к временному объекту. Например, следующий фрагмент
Point3d p3; |
трансформируется в такой:
// Псевдокод на C++ Point3d temp; operator+( temp, p1, p2 ); p3.Point3d::operator=( temp ); |
Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением
(return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге “Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем.
15
Forekc.ru
Рефераты, дипломы, курсовые, выпускные и квалификационные работы, диссертации, учебники, учебные пособия, лекции, методические пособия и рекомендации, программы и курсы обучения, публикации из профильных изданий