*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

понедельник, 2 февраля 2009 г.

Функциональный деструктор в С++

В С++ крайне распространен прием использования класса std::vector для хранения указателей на размещенные в куче объекты.

Например, вы размещаете в куче десяток объектов типа Книга (Book) и сохраняете указатели на них в векторе:
class Book {
public:
Book(int index);
...
};
...
std::vector<Book *> books;
for (int i = 0; i < 10; ++i)
books.push_back(new Book(i));
Естественно, после использования память надо освободить. Обычно стандартный прием для этого таков:
for (std::vector<Book *>::iterator i = books.begin(); i != books.end(); ++i)
delete *i
В целом, с таким подходом все в порядке, разве что слегка веет от него излишней алгоритмической загруженностью. Он вынужден, навязан особенностями языка C++. Индексная переменная i здесь абсолютно неважна для цикла, она является чисто служебной. Все это, конечно, не так страшно, как использование оператора goto или статических переменных, но все равно хочется гармонии. И способ есть. Данный код можно переписать так:
#include <algorithm>
...
class deleter {
public:
template <typename T>
void operator()(const T* p) const {
delete p;
}
};
...
std::for_each(a.begin(), a.end(), deleter());
Данный код определяет класс-функтор, у которого перегруженный оператор void operator()(...) является шаблонным. Затем стандартный алгоритм std::for_each() вызывает этот оператор для каждого члена вектора.
Конечно, вы можете сказать, что мол битва за идею принуждает нас таскать за собой класс deleter, но тут аргумент простой — данный подход ближе к декларативному подходу в программировании, нежели к прямому алгоритмическому. В декларативном подходе вы стараетесь как можно больше логики перенести из ее явного программирования базовыми конструкциями типа условий, циклов и т.д. к ее выражению через определения (декларации) сущностей и их взаимосвязей. Декларативные конструкции проще дробить на независимые куски, а значит проще тестировать. Например, вы можете протестировать алгоритм std::for_each в изоляции, тем самым гарантируя его корректную работу сразу во всей программе, а вот протестировать явный цикл в изоляции вряд ли получится, так как цикл "жестко вплетен" в прочую логику программы. Максимум удастся проверить данный конкретный цикл как-то вручную, и если их программе много, проверять придется каждый из них.
Соглашусь, однако, что конкретно этот пример весьма тривиален и является в большинстве делом вкуса, нежели вопросом реального выигрыша простоте и тестируемости кода. Но сам прием весьма показателен в плане замены простейших алгоритмов высокоуровневыми сущностями. И еще, в защиту такого приему могу сказать, что например, вы можете переопределить алгоритм std::for_each на свой, который сможет на конкретно вашей платформе выполняться гораздо быстрее, или, например, ловить исключения работы с кучей и журналировать проблемы освобождения памяти. В случае же прямого использования цикла for вам придется переписать сам цикл. Хорошо, когда такое место одно в программе, а если их тысячи?

3 комментария:

  1. хороший прием.
    но нужно стараться придерживаться идимы RAII для обсепечения exception safety.

    А потому приходится писать "умные" контейнеры, а в них уже да, можно и такой for_each намутить.

    ОтветитьУдалить
  2. Проще использовать share_ptr

    ОтветитьУдалить
  3. В целом да, RAII - это правильный путь.

    ОтветитьУдалить