84. Предпочитайте вызовы алгоритмов самостоятельно разрабатываемым циклам
84. Предпочитайте вызовы алгоритмов самостоятельно разрабатываемым циклам
Резюме
Разумно используйте функциональные объекты. В очень простых случаях написанные самостоятельно циклы могут оказаться более простым и эффективным решением. Тем не менее, вызов алгоритма вместо самостоятельно разработанного цикла может оказаться более выразительным, легче сопровождаемым, менее подверженным ошибкам и не менее эффективным.
При вызове алгоритма подумайте о написании собственного функционального объекта, который инкапсулирует всю необходимую логику. Избегайте объединения связывателей параметров и простых функциональных объектов (например, bind2nd, plus), что обычно снижает ясность кода. Подумайте об использовании лямбда-библиотеки [Boost], которая автоматизирует задачу написания функциональных объектов.
Обсуждение
Программы, которые используют STL, имеют тенденцию к меньшему количеству явных циклов, чем у программ без применения STL благодаря замене низкоуровневых циклов высокоуровневыми абстрактными операциями с существенно большей семантикой. При программировании с использованием STL, следует думать не в категориях "как обработать каждый элемент", а "как обработать диапазон элементов".
Главное преимущество алгоритмов и шаблонов проектирования в общем случае заключается в том, что они позволяют нам говорить на более высоком уровне абстракции. В современном программировании мы не говорим "пусть несколько объектов следят за одним объектом и получают автоматические уведомления при изменении его состояния". Вместо этого мы говорим просто о "шаблоне проектирования Observer". Аналогично, мы говорим "Bridge", "Factory" и "Visitor". Использование словаря шаблонов проектирования позволяет повысить уровень, эффективность и корректность нашего обсуждения. Точно так же при использовании алгоритмов мы не говорим "выполняем действие над каждым элементом диапазона и записываем результат в некоторое место"; вместо этого мы говорим просто — transform. Аналогично, мы можем сказать for_each, replace_if и partition. Алгоритмы, подобно шаблонам проектирования, самодокументируемы. "Голые" циклы for и while ничего не говорят о том, для чего они предназначены, и читателю приходится изучать тела циклов, чтобы расшифровать их семантику.
Алгоритмы обычно более корректны, чем циклы. В разрабатываемых самостоятельно циклах легко допустить ошибку, например, такую как использование недействительных итераторов (см. рекомендации 83 и 99); алгоритмы в библиотеке отлажены на предмет использования недействительных итераторов и других распространенных ошибок.
И наконец, алгоритмы зачастую также более эффективны, чем простые циклы (см. [Sutter00] и [Meyers01]). В них устранены небольшие неэффективности, такие как повторные вычисления container.end()). Не менее важно, что стандартные алгоритмы, используемые вами, были реализованы теми же программистами, кто реализовывал и используемые вами стандартные контейнеры, и понятно, что их знание конкретных реализаций позволяет им написать алгоритмы более эффективно, чем это сможете сделать вы. Важнее всего, однако, то, что многие алгоритмы имеют высокоинтеллектуальные реализации, которые мы никогда не сможем реализовать в собственноручно разработанном коде (не считая случаев, когда нам не нужна та степень обобщенности, которую предоставляют алгоритмы). Вообще говоря, более используемая библиотека всегда оказывается лучше отлаженной и более эффективной просто потому, что у нее больше пользователей. Вряд ли вы найдете и сможете использовать в своей программе библиотеку, настолько же широко применяемую, как и реализация вашей стандартной библиотеки. Воспользуйтесь ею. Алгоритмы STL уже написаны — так почему бы не воспользоваться ими?
Подумайте об использовании лямбда-функций [Boost]. Лямбда-функции представляют собой важный инструмент, который позволяет справиться с основным недостатком алгоритмов, а именно с удобочитаемостью. Без их применения вы должны использовать либо функциональные объекты (но тогда тела даже простых циклов находятся в отдельном месте, далеко от точки вызова), либо стандартные связыватели и функциональные объекты наподобие bind2nd и plus (достаточно запутанные, сложные и утомительные в использовании).
Примеры
Вот два примера, адаптированных из [Meyers01].
Пример 1. Преобразование deque. После того как было выполнено несколько некорректных итераций из-за недействительных итераторов (например, см. рекомендацию 83), мы пришли к окончательной версии цикла для прибавления 41 к каждому элементу массива данных типа doublе и помещения результата в дек deque<doublе>:
deque<double>::iterator current = d.begin();
for (size_t i =0; i < max; ++i) {
// Сохраняем current действительным
current = d.insert(current, data[i] + 41);
++current; // Увеличиваем его, когда это
} // становится безопасным
Вызов алгоритма позволяет легко обойти все ловушки в этом коде:
transform(
data.begin(), data.end(), // Копируем элементы
data inserter(d, d.begin()), // в d с начала контейнера,
bind2nd(plus<double>(),41)); // добавляя к каждому 41
Впрочем, bind2nd и plus достаточно неудобны. Откровенно говоря, в действительности их мало кто использует, и связано это в первую очередь с плохой удобочитаемостью такого кода (см. рекомендацию 6).
При использовании лямбда-функций, генерирующих для нас функциональные объекты, мы можем написать совсем простой код:
transform(data, data+max, inserter(d,d.begin()), _1 + 41);
Пример 2. Найти первый элемент между x и у. Рассмотрим простой цикл, который выполняет поиск в vector<int> v первого элемента, значение которого находится между x и y. Он вычисляет итератор, который указывает либо на найденный элемент, либо на v.end():
vector<int>::iterator i = v.begin();
for (; i != v.end(); ++i)
if (*i > x && *i < y) break;
Вызов алгоритма достаточно проблематичен. При отсутствии лямбда-функций у нас есть два варианта — написание собственного функционального объекта или использование стандартных связывателей. Увы, в последнем случае мы не можем обойтись только стандартными связывателями и должны использовать нестандартный (хотя и достаточно распространенный) адаптер compose2, но даже в этом случае код получается совершенно непонятным, так что такой код на практике никто просто не напишет:
vector<int>::iterator i =
find_if(v.begin(), v.end(),
compose2(logical_and<bool>(),
bind2nd(greater<int>(), x), bind2nd(less<int>(), y)));
Другой вариант, а именно — написание собственного функционального объекта — достаточно жизнеспособен. Он достаточно хорошо выглядит в точке вызова, а главный его недостаток— необходимость написания функционального объекта BetweenValues, который визуально удаляет логику из точки вызова:
template<typename T>
class BetweenValues : public unary_function<T, bool> {
public:
BetweenValues(const T& low, const T& high)
: low_(low), high_(high) { }
bool operator()(const T& val) const
{ return val > low_ && val < high_; }
private:
T low_, high_;
};
vector<int>::iterator i =
find_if( v.begin(), v.end(), BetweenValues<int>(x, y));
При применении лямбда-функций можно написать просто:
vector<int>::iterator i =
find_if(v.begin(), v.end(), _1 > x && _1 < y);
Исключения
При использовании функциональных объектов тело цикла оказывается размещено в некотором месте, удаленном от точки вызова, что затрудняет чтение исходного текста. (Использование простых объектов со стандартными и нестандартными связывателями представляется нереалистичным.)
Лямбда-функции [Boost] решают проблему и надежно работают на современных компиляторах, но они не годятся для более старых компиляторов и могут выдавать большие запутанные сообщения об ошибках при некорректном использовании. Вызов же именованных функций, включая функции-члены, все равно требует синтаксиса с использованием связывателей.
Ссылки
[Allison98] §15 • [Austern99] §11-13 • [Boost] Lambda library • [McConnell93] §15 • [Meyers01] §43 • [Musser01] §11 • [Stroustrup00] §6.1.8, §18.5.1 • [Sutter00] §7
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
(7.1) Машина с atx блоком питания не выключает питание самостоятельно.
(7.1) Машина с atx блоком питания не выключает питание самостоятельно. Основным режимом работы для W2k считается ACPI режим, и именно вокруг ACPI построено управление питанием. Проблема в том, что до выхода W2k все возможности ACPI нигде толком не использовались. Поэтому
6.2. Машина с ATX блоком питания не выключает питание самостоятельно.
6.2. Машина с ATX блоком питания не выключает питание самостоятельно. Основным режимом работы для XP считается ACPI режим, и именно вокруг ACPI построено управление питанием. Проблема в том, что до выхода W2k и XP все возможности ACPI нигде толком не использовались. Поэтому
Как самостоятельно распознать наличие в компьютере шпионского ПО?
Как самостоятельно распознать наличие в компьютере шпионского ПО? Отличительной чертой Spyware является то, что их трудно распознать с помощью штатных антивирусных программ. Поэтому для борьбы с ними рекомендуется использовать специальные утилиты, которые во множестве
28. Предпочитайте канонический вид ++ и --, и вызов префиксных операторов
28. Предпочитайте канонический вид ++ и --, и вызов префиксных операторов РезюмеОсобенность операторов инкремента и декремента состоит в том, что у них есть префиксная и постфиксная формы с немного отличающейся семантикой. Определяйте операторы operator++ и operator-- так, чтобы они
33. Предпочитайте минимальные классы монолитным
33. Предпочитайте минимальные классы монолитным РезюмеРазделяй и властвуй: небольшие классы легче писать, тестировать и использовать. Они также применимы в большем количестве ситуаций. Предпочитайте такие небольшие классы, которые воплощают простые концепции, классам,
34. Предпочитайте композицию наследованию
34. Предпочитайте композицию наследованию РезюмеИзбегайте "налога на наследство": наследование — вторая по силе после отношения дружбы взаимосвязь, которую можно выразить в С++. Сильные связи нежелательны, и их следует избегать везде, где только можно. Таким образом,
36. Предпочитайте предоставление абстрактных интерфейсов
36. Предпочитайте предоставление абстрактных интерфейсов РезюмеВы любите абстракционизм? Абстрактные интерфейсы помогают вам сосредоточиться на проблемах правильного абстрагирования, не вдаваясь в детали реализации или управления состояниями. Предпочтительно
48. В конструкторах предпочитайте инициализацию присваиванию
48. В конструкторах предпочитайте инициализацию присваиванию РезюмеВ конструкторах использование инициализации вместо присваивания для установки значений переменных-членов предохраняет от ненужной работы времени выполнения при том же объеме вводимого исходного
55. Предпочитайте канонический вид присваивания
55. Предпочитайте канонический вид присваивания РезюмеПри реализации оператора operator= предпочитайте использовать канонический вид — невиртуальный с определенной сигнатурой.ОбсуждениеПредпочтительно объявлять копирующее присваивание для типа T с одной из следующих
Как самостоятельно распознать наличие в компьютере программ-шпионов
Как самостоятельно распознать наличие в компьютере программ-шпионов Отличительной чертой Spyware является то, что их трудно распознать с помощью штатных антивирусных программ. Для борьбы с ними предназначены специальные утилиты, которые можно скачать в Интернете. Но
Совет 32. Сопровождайте вызовы remove-подобных алгоритмов вызовом erase
Совет 32. Сопровождайте вызовы remove-подобных алгоритмов вызовом erase Начнем с краткого обзора remove, поскольку этот алгоритм вызывает больше всего недоразумений в STL. Прежде всего необходимо рассеять все сомнения относительно того, что делает алгоритм remove, а также почему и как
Собираем ноутбук самостоятельно
Собираем ноутбук самостоятельно Первыми выпустила портативные компьютеры Compaq (сейчас принадлежит Hewlett-Packard). Назывались они лэптопами, имея габариты чемодана, ручку для переноски и встроенный в крышку ЭЛТ-монитор (фактически использовались настольные компоненты, только
Глава 3 Как самостоятельно соединить компоненты компьютера
Глава 3 Как самостоятельно соединить компоненты компьютера 3.1. Что надо знать, чтобы самостоятельно правильно собрать компьютер Вы выбрали компьютер, оплатили его и принесли домой несколько коробок. Что делать дальше?Прежде всего, распакуйте комплектующие. Если на улице
Глава 4 Самостоятельно осваиваем работу с клавиатурой и мышью
Глава 4 Самостоятельно осваиваем работу с клавиатурой и мышью 4.1. Как не запутаться в кнопках клавиатуры В этой главе мы поговорим только о назначении стандартных клавиш, которые есть на любой клавиатуре, как бы она ни выглядела. А о назначении дополнительных клавиш вы
Глава 9 Как самостоятельно устанавливать и удалять программы
Глава 9 Как самостоятельно устанавливать и удалять программы 9.1. Устанавливаем программы Прежде чем начать работу с программой, нужно ее установить. Конечно, это не касается программ, уже имеющихся на вашем компьютере. Хотя обычно нужно устанавливать почти все программы,