На практике с C ++, что такое RAII , что такое интеллектуальные указатели , как они реализованы в программе и каковы преимущества использования RAII с интеллектуальными указателями?
На практике с C ++, что такое RAII , что такое интеллектуальные указатели , как они реализованы в программе и каковы преимущества использования RAII с интеллектуальными указателями?
Ответы:
Простой (и, возможно, часто используемый) пример RAII - это класс File. Без RAII код может выглядеть примерно так:
File file("/path/to/file");
// Do stuff with file
file.close();
Другими словами, мы должны убедиться, что закрыли файл, как только закончили с ним. У этого есть два недостатка - во-первых, где бы мы ни использовали File, нам придется вызывать File :: close () - если мы забудем сделать это, мы держим файл дольше, чем нужно. Вторая проблема заключается в том, что если перед закрытием файла возникает исключение?
Java решает вторую проблему, используя предложение finally:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
или начиная с Java 7, оператор try-with-resource:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ решает обе проблемы с помощью RAII, то есть закрывает файл в деструкторе File. До тех пор, пока объект File уничтожается в нужное время (что и должно быть), закрытие файла позаботится о нас. Итак, наш код теперь выглядит примерно так:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Это не может быть сделано в Java, поскольку нет гарантии, когда объект будет уничтожен, поэтому мы не можем гарантировать, когда такой ресурс, как файл, будет освобожден.
На умные указатели - большую часть времени мы просто создаем объекты в стеке. Например (и украсть пример из другого ответа):
void foo() {
std::string str;
// Do cool things to or using str
}
Это работает нормально - но что, если мы хотим вернуть str? Мы могли бы написать это:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Так что с этим не так? Ну, тип возвращаемого значения - std :: string - значит, мы возвращаемся по значению. Это означает, что мы копируем str и фактически возвращаем копию. Это может быть дорого, и мы можем избежать затрат на его копирование. Следовательно, мы можем прийти к идее возврата по ссылке или по указателю.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
К сожалению, этот код не работает. Мы возвращаем указатель на str - но str был создан в стеке, поэтому мы будем удалены после выхода из foo (). Другими словами, к тому моменту, когда вызывающий объект получает указатель, он становится бесполезным (и, возможно, хуже, чем бесполезным, поскольку его использование может привести к всевозможным ошибкам)
Итак, каково решение? Мы можем создать str в куче, используя new - таким образом, когда foo () завершится, str не будет уничтожен.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Конечно, это решение тоже не идеально. Причина в том, что мы создали str, но никогда не удаляем его. Это не может быть проблемой в очень маленькой программе, но в целом мы хотим убедиться, что мы удалили ее. Мы могли бы просто сказать, что вызывающая сторона должна удалить объект, как только закончит с ним. Недостатком является то, что вызывающая сторона должна управлять памятью, что добавляет дополнительную сложность и может ошибиться, что приведет к утечке памяти, то есть к удалению объекта, даже если он больше не требуется.
Вот где приходят умные указатели. В следующем примере используется shared_ptr - я предлагаю вам взглянуть на различные типы умных указателей, чтобы узнать, что вы на самом деле хотите использовать.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Теперь shared_ptr посчитает количество ссылок на str. Например
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Теперь есть две ссылки на одну и ту же строку. Как только не останется ссылок на str, он будет удален. Таким образом, вам больше не нужно беспокоиться об удалении его самостоятельно.
Быстрое редактирование: как отмечалось в некоторых комментариях, этот пример не идеален (по крайней мере!) По двум причинам. Во-первых, из-за реализации строк копирование строки обычно обходится недорого. Во-вторых, из-за того, что называется оптимизацией именованных возвращаемых значений, возврат по значению может быть не дорогостоящим, поскольку компилятор может сделать некоторую хитрость, чтобы ускорить процесс.
Итак, давайте попробуем другой пример, используя наш класс File.
Допустим, мы хотим использовать файл в качестве журнала. Это означает, что мы хотим открыть наш файл только в режиме добавления:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Теперь давайте установим наш файл как журнал для пары других объектов:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
К сожалению, этот пример заканчивается ужасно - файл будет закрыт, как только этот метод завершится, а это означает, что foo и bar теперь имеют неверный файл журнала. Мы можем создать файл в куче и передать указатель на файл как foo, так и bar:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Но тогда кто несет ответственность за удаление файла? Если ни один из файлов не удалить, то у нас утечка памяти и ресурсов. Мы не знаем, завершится ли файл foo или bar первым, поэтому мы не можем ожидать, что файл будет удален и сами. Например, если foo удаляет файл до того, как bar закончит с ним, bar теперь имеет недопустимый указатель.
Итак, как вы уже догадались, мы могли бы использовать умные указатели, чтобы выручить нас.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Теперь никому не нужно беспокоиться об удалении файла - как только foo и bar завершат работу и у них больше не будет ссылок на файл (вероятно, из-за уничтожения foo и bar), файл будет автоматически удален.
RAII Это странное название для простой, но удивительной концепции. Лучше название Scope Bound Resource Management (SBRM). Идея состоит в том, что вам часто приходится выделять ресурсы в начале блока, и вам необходимо освободить его при выходе из блока. Выход из блока может происходить при обычном управлении потоком, выпрыгивании из него и даже при исключении. Чтобы покрыть все эти случаи, код становится более сложным и избыточным.
Просто пример, делающий это без SBRM:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
Как видите, есть много способов, которыми мы можем стать pwned. Идея состоит в том, что мы инкапсулируем управление ресурсами в класс. Инициализация его объекта получает ресурс («Приобретение ресурса - Инициализация»). Когда мы выходим из блока (область видимости блока), ресурс снова освобождается.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
Это хорошо, если у вас есть свои собственные классы, которые предназначены не только для распределения / освобождения ресурсов. Распределение будет просто дополнительной заботой, чтобы сделать их работу. Но как только вы просто захотите распределить / освободить ресурсы, вышеприведенное становится неудобным. Вы должны написать класс обертки для каждого вида ресурсов, которые вы приобретаете. Чтобы облегчить это, умные указатели позволяют автоматизировать этот процесс:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Обычно умные указатели - это тонкие обертки вокруг new / delete, которые просто вызывают, delete
когда ресурс, которым они владеют, выходит за пределы области видимости. Некоторые умные указатели, такие как shared_ptr, позволяют вам сообщать им так называемое средство удаления, которое используется вместо delete
. Это позволяет вам, например, управлять дескрипторами окна, ресурсами регулярных выражений и другими произвольными вещами, если вы сообщаете shared_ptr о правильном удалителе.
Существуют разные умные указатели для разных целей:
Код:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
В отличие от auto_ptr, unique_ptr может быть помещен в контейнер, потому что контейнеры смогут содержать не копируемые (но подвижные) типы, такие как streams и unique_ptr.
Код:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Код:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
Как видите, сюжет-источник (функция fx) является общим, но у каждого есть отдельная запись, для которой мы устанавливаем цвет. Существует класс weak_ptr, который используется, когда код должен ссылаться на ресурс, принадлежащий интеллектуальному указателю, но не должен владеть ресурсом. Вместо того, чтобы передавать необработанный указатель, вы должны создать слабый_птр. Он сгенерирует исключение, когда заметит, что вы пытаетесь получить доступ к ресурсу по пути доступа weak_ptr, даже если нет ресурса shared_ptr, владеющего ресурсом.
unique_ptr
, и sort
также будут изменены также.
RAII - это парадигма проектирования, гарантирующая, что переменные обрабатывают всю необходимую инициализацию в своих конструкторах и всю необходимую очистку в своих деструкторах. Это сводит всю инициализацию и очистку к одному шагу.
C ++ не требует RAII, но все чаще признается, что использование методов RAII даст более надежный код.
Причина, по которой RAII полезен в C ++, заключается в том, что C ++ по сути управляет созданием и уничтожением переменных, когда они входят и выходят из области видимости, либо через обычный поток кода, либо через разматывание стека, инициируемое исключением. Это халява в C ++.
Связывая всю инициализацию и очистку с этими механизмами, вы гарантируете, что C ++ позаботится об этой работе и за вас.
Разговор о RAII в C ++ обычно приводит к обсуждению умных указателей, потому что указатели особенно хрупки, когда дело доходит до очистки. При управлении выделенной из кучи памяти, полученной из malloc или new, программист обычно обязан освободить или удалить эту память до уничтожения указателя. Интеллектуальные указатели будут использовать философию RAII, чтобы гарантировать, что выделенные объекты кучи будут уничтожены каждый раз, когда уничтожается переменная-указатель.
Умный указатель является вариацией RAII. RAII означает, что получение ресурсов является инициализацией. Умный указатель получает ресурс (память) перед использованием, а затем автоматически выбрасывает его в деструктор. Происходят две вещи:
Например, другой пример - сетевой сокет RAII. В таком случае:
Теперь, как вы можете видеть, RAII является очень полезным инструментом в большинстве случаев, так как помогает людям быть уволенным.
Источники умных указателей на C ++ исчисляются миллионами по всей сети, включая ответы выше меня.
В Boost есть несколько таких, в том числе в Boost.Interprocess для общей памяти. Это значительно упрощает управление памятью, особенно в ситуациях, вызывающих головную боль, например, когда у вас 5 процессов, совместно использующих одну и ту же структуру данных: когда у всех есть кусок памяти, вы хотите, чтобы он автоматически освобождался и не нужно было сидеть, пытаясь понять кто должен отвечать за вызов delete
фрагмента памяти, чтобы в результате не возникла утечка памяти или указатель, который дважды по ошибке освобождается и может повредить всю кучу.
void foo () { std :: string bar; // // больше кода здесь // }
Независимо от того, что произойдет, панель будет должным образом удалена после того, как область функции foo () останется позади.
Внутренне реализации std :: string часто используют указатели с подсчетом ссылок. Таким образом, внутреннюю строку необходимо копировать только тогда, когда одна из копий строк изменилась. Поэтому умный указатель с подсчетом ссылок позволяет копировать что-либо только при необходимости.
Кроме того, подсчет внутренних ссылок позволяет правильно удалять память, когда копия внутренней строки больше не нужна.