RAII и умные указатели в C ++


Ответы:


317

Простой (и, возможно, часто используемый) пример 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), файл будет автоматически удален.


7
Следует отметить, что многие реализации строк реализованы в виде указателя с подсчетом ссылок. Эта семантика копирования при записи делает возврат строки по значению действительно недорогим.

7
Даже для тех, которые этого не делают, многие компиляторы реализуют оптимизацию NRV, которая позаботится о накладных расходах. В общем, я считаю shared_ptr редко полезным - просто придерживайтесь RAII и избегайте совместного владения.
Неманья Трифунович

27
возврат строки не является хорошей причиной для использования умных указателей. Оптимизация возвращаемого значения может легко оптимизировать возвращаемое значение, а семантика перемещения c ++ 1x полностью исключит копию (при правильном использовании). Вместо этого
покажите

1
Я думаю, что ваше заключение о том, почему Java не может сделать это, недостаточно ясно. Самый простой способ описать это ограничение в Java или C # состоит в том, что нет способа выделить его в стеке. C # позволяет выделение стека через специальное ключевое слово, однако вы теряете тип saftey.
ApplePieIsGood

4
@Nemanja Trifunovic: Под RAII в этом контексте вы имеете в виду возврат копий / создание объектов в стеке? Это не работает, если у вас есть возврат / принятие объектов типов, которые можно разделить на подклассы. Затем вы должны использовать указатель, чтобы не разрезать объект, и я бы сказал, что в таких случаях умный указатель часто лучше, чем необработанный.
Фрэнк Остерфельд

141

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

это умный указатель, который владеет исключительно объектом. Это не в поддержку, но, скорее всего, появится в следующем стандарте C ++. Это не для копирования, но поддерживает передачу права собственности . Пример кода (следующий C ++):

Код:

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.

scoped_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

для совместной собственности. Для этого это и копируемый и подвижный. Несколько экземпляров умного указателя могут владеть одним и тем же ресурсом. Как только последний умный указатель, владеющий ресурсом, выйдет из области видимости, ресурс будет освобожден. Пример из одного из моих реальных проектов:

Код:

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, владеющего ресурсом.


Насколько я знаю, не копируемые объекты вообще не годятся для использования в контейнерах stl, поскольку они основаны на семантике значений - что произойдет, если вы захотите отсортировать этот контейнер? sort копирует элементы ...
fmuecke

Контейнеры C ++ 0x будут изменены таким образом, чтобы они соответствовали типам только для перемещения unique_ptr, и sortтакже будут изменены также.
Йоханнес Шауб -

Вы помните, где вы впервые услышали термин SBRM? Джеймс пытается выследить это.
GManNickG

какие заголовки или библиотеки я должен включить, чтобы использовать их? какие-либо дальнейшие чтения по этому поводу?
atoMerz

Один совет здесь: если есть ответ на вопрос C ++ от @litb, это правильный ответ (независимо от того, были ли голоса или ответ помечен как «правильный») ...
Fnl

32

Посылка и причины просты, в понятии.

RAII - это парадигма проектирования, гарантирующая, что переменные обрабатывают всю необходимую инициализацию в своих конструкторах и всю необходимую очистку в своих деструкторах. Это сводит всю инициализацию и очистку к одному шагу.

C ++ не требует RAII, но все чаще признается, что использование методов RAII даст более надежный код.

Причина, по которой RAII полезен в C ++, заключается в том, что C ++ по сути управляет созданием и уничтожением переменных, когда они входят и выходят из области видимости, либо через обычный поток кода, либо через разматывание стека, инициируемое исключением. Это халява в C ++.

Связывая всю инициализацию и очистку с этими механизмами, вы гарантируете, что C ++ позаботится об этой работе и за вас.

Разговор о RAII в C ++ обычно приводит к обсуждению умных указателей, потому что указатели особенно хрупки, когда дело доходит до очистки. При управлении выделенной из кучи памяти, полученной из malloc или new, программист обычно обязан освободить или удалить эту память до уничтожения указателя. Интеллектуальные указатели будут использовать философию RAII, чтобы гарантировать, что выделенные объекты кучи будут уничтожены каждый раз, когда уничтожается переменная-указатель.


Кроме того - указатели являются наиболее распространенным применением RAII - вы, вероятно, выделите в тысячи раз больше указателей, чем любой другой ресурс.
Затмение

8

Умный указатель является вариацией RAII. RAII означает, что получение ресурсов является инициализацией. Умный указатель получает ресурс (память) перед использованием, а затем автоматически выбрасывает его в деструктор. Происходят две вещи:

  1. Мы выделяем память перед тем, как ее использовать, всегда, даже когда нам это не нравится - трудно сделать другой путь с помощью умного указателя. Если этого не произошло, вы попытаетесь получить доступ к NULL-памяти, что приведет к падению (очень болезненно).
  2. Мы освобождаем память даже в случае ошибки. Нет памяти осталось висеть.

Например, другой пример - сетевой сокет RAII. В таком случае:

  1. Мы открываем сетевой сокет до того, как используем его, всегда, даже когда нам не хочется - это сложно сделать по-другому с RAII. Если вы попытаетесь сделать это без RAII, вы можете открыть пустой сокет для, скажем, MSN-соединения. Тогда сообщение типа «давайте сделаем это сегодня вечером» может не быть передано, пользователи не будут уволены, и вы рискуете быть уволенным.
  2. Мы закрываем сетевой сокет даже в случае ошибки. Сокет не остается висящим, так как это может помешать ответному сообщению «наверняка я попаду внизу» от ответного удара по отправителю.

Теперь, как вы можете видеть, RAII является очень полезным инструментом в большинстве случаев, так как помогает людям быть уволенным.

Источники умных указателей на C ++ исчисляются миллионами по всей сети, включая ответы выше меня.


2

В Boost есть несколько таких, в том числе в Boost.Interprocess для общей памяти. Это значительно упрощает управление памятью, особенно в ситуациях, вызывающих головную боль, например, когда у вас 5 процессов, совместно использующих одну и ту же структуру данных: когда у всех есть кусок памяти, вы хотите, чтобы он автоматически освобождался и не нужно было сидеть, пытаясь понять кто должен отвечать за вызов deleteфрагмента памяти, чтобы в результате не возникла утечка памяти или указатель, который дважды по ошибке освобождается и может повредить всю кучу.


0
void foo ()
{
   std :: string bar;
   //
   // больше кода здесь
   //
}

Независимо от того, что произойдет, панель будет должным образом удалена после того, как область функции foo () останется позади.

Внутренне реализации std :: string часто используют указатели с подсчетом ссылок. Таким образом, внутреннюю строку необходимо копировать только тогда, когда одна из копий строк изменилась. Поэтому умный указатель с подсчетом ссылок позволяет копировать что-либо только при необходимости.

Кроме того, подсчет внутренних ссылок позволяет правильно удалять память, когда копия внутренней строки больше не нужна.


1
void f () {Obj x; } Obj x удаляется с помощью создания / уничтожения кадра стека (разматывания) ... это не связано с подсчетом ссылок.
Эрнан

Подсчет ссылок является функцией внутренней реализации строки. RAII - это концепция удаления объекта, когда объект выходит из области видимости. Вопрос был о RAII, а также об умных указателях.

1
«Неважно, что происходит» - что произойдет, если перед возвратом функции возникнет исключение?
titaniumdecoy

Какая функция возвращается? Если в foo выдается исключение, то панель удаляется. Конструктор по умолчанию для бара, создающего исключение, будет необычным событием.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.