|
Программирование >> Оптимизация возвращаемого значения
была бы для вас последним делом - но только до тех пор, пока вы не услышали бы, как родители идут, чтобы посмотреть на наведенный порядок. Тогда вы бросились бы в свою комнату и сразу же принялись за работу. Но если бы вам повезло, и родители не стали проверять, как выполнено их поручение, то вам вообще удалось бы избежать уборки комнаты. Оказывается, тактика задержки , которой вовсю пользуется пятилетний ребенок, характерна и для программиста, работающего с С++. В информатике такая тактика называется отложеннъш (буквально ленивым ) вычислением (lazy evaluation). При использовании отложенных вычислений классы записываются таким образом, что вычисления не производятся до тех пор, пока не потребуются их результаты. Если результаты не потребуются никогда, то эти вычисления никогда и не будут выполнены. Таким образом, ни пользователи вашего программного обеспечения, ни ваши родители не могли бы выбрать более мудрый путь. Отложенное вычисление применимо для множества приложений, но в настоящей главе описываются только четыре наиболее частых случая. Подсчет ссылок Рассмотрим следующую программу: class String {...}; Класс string (стандартный тип string может быть реализован, как описано ниже, но это не обязательно) . String si = Hello ; String s2 = si; Вызывает конструктор копирования String. Обычная реализация конструктора копирования String привела бы к созданию объектов si и s2, каждый из которых имел бы собственную копию строки Hello после инициализации s2 значением из si. Это повлекло бы за собой довольно большие расходы, ведь конструктору пришлось бы скопировать значение si, чтобы передать его s2, а это обычно требует выделения динамической памяти при помощи оператора new (см. правило 8) и вызова функции strcpy для копирования данных из si в память, выделенную для s2. Таким образом, будет производиться энергичное вычисление (eager evaluation): создание копии sin помещение ее в s2 только потому, что был вызван конструктор копирования string. Однако в данном случае не было реальной необходимости в том, чтобы в s2 находилась копия значения, так как s2 еще совсем не использовался. При ленивом подходе выполняется намного меньше работы. Вместо передачи объекту s2 копии значения si, объект s2 разделяет значение si. Достаточно знать, кто и что совместно использует, и это позволит избавиться от затрат на вызов оператора new и копирование. То, что структура данных совместно используется объектами si и s2, прозрачно для клиентов, и, конечно, не вносит различий в подобные операторы, поскольку они только считывают значения, а не записывают их: cout si; Считывает значение si. cout si + s2; Считывает значение si и s2 . Фактически, совместное использование значений существенно, только когда происходит модификация той или иной строки; в этом случае важно, чтобы изменения вносились только в одну строку, но не в обе. В операторе s2.convertToUpperCase(); ДОЛЖНО меняться только значение s2, а не значение si. Для обработки подобных операторов нужно реализовать функцию convertToUpperCase объекта String так, чтобы она копировала значение s2, а в самой s2 до модификации делала это значение закрытым. Внутри функции ConvertToUpperCase вы не можете больше придерживаться ленивой стратегии: необходимо сделать копию значения s2 (совместно используемую) для использования внутри s2. С другой стороны, если объект s2 никогда не изменяется, вам не понадобится делать закрытую копию его значения. Значение может использоваться совместно, пока оно существует. Если вам повезет, то s2 никогда не будет изменяться, и тогда вообще не придется задавать ему собственное значение. Более подробно о совместном использовании значений (включая весь код) рассказано в правиле 29, однако, надеюсь, основная идея ясна вам уже сейчас: надо делать копию чего-либо только в тот момент, когда в ней возникнет насущная необходимость. Будьте ленивыми - используйте любую подходящую копию, пока это можно делать безнаказанно (в некоторых случаях такое состояние длится бесконечно). Как отличить считывание от записи Из5ая далее пример использования строк для подсчета ссылок, можно столкнуться со второй ситуацией, в которой будет полезно отложенное вычисление. Рассмотрим следующую программу: string S = Homers Iliad ; Строка s содержит ссылку. cout << s[3] ; Вызов operator [] для считывания s[3] . s[3]= X; Вызов operator[] для записи s[3]. Первый вызов operator [ ] соответствует считыванию части строки, второй -выполнению записи. Ваша задача - научиться различать вызов оператора чтения и вызов оператора записи, так как чтение строки со ссылками выполнить просто, а для записи в такую строку может потребоваться предварительное создание новой копии значения строки. Это затрудняет реализацию. Чтобы достигнуть поставленной цели, необходимо выполнять различные преобразования внутри operator [ ] (в зависимости от того, вызывается ли он для выполнения чтения или записи). Как же определить, был ли operator [ ] вызван в контексте чтения или записи? К сожалению, это сделать нельзя. Но используя отложенное вычисление и proxy-классы, как описано в правиле 30, вы, тем не менее, сможете отложить решение, осуществлять ли операцию чтения или записи, пока не определите, какое действие является правильным. Отложенная выборка Теперь третий пример отложенного вычисления. Представьте, что у вас есть программа, которая использует большие объекты, содержащие много полей. Эти объекты должны существовать после завершения работы программы, поэтому они хранятся в базе данных. Каждый объект имеет уникальный идентификатор, который может использоваться для извлечения объекта из базы: class LargeObject { Большой постоянный объект, public: LargeObject (ObjectID id) ; Восстановить объект с диска. const strings fieldlO const; Значение поля 1. int field2() const; Значение поля 2. double fields 0 const; II ... const strings field4() const; const strings fields() const; Теперь рассмотрим затраты на восстановление объекта LargeObject с диска. void restoreAndProcessObject(ObjectID id) { LargeObject object (id) ; Восстановление объекта. Поскольку экземпляры объекта LargeOb j ect велики, пол5ение всех данных для такого объекта может оказаться достаточно сложной операцией, особенно, если информация должна быть извлечена из удаленной базы и передана по сети. В некоторых случаях чтение всех данных и не требуется. Например, рассмотрим такое приложение: void restoreAndProcessObject(ObjectID id) LargeObject object(id); if (object.field2 0 == 0) { cout Object id : null field2.\n ; } В этом случае требуется лишь значение f ield2, а любые попытки установить значение других полей бесполезны. Ленивый подход к описываемой проблеме заключается в том, чтобы не считывать данные с диска при создании объекта LargeOb j ect. Таким образом, вместо объекта создается только его оболочка , и данные извлекаются из базы
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |