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

На практике с C++, что такое RAII, что такое интеллектуальные указатели, как они реализованы в программе и каковы преимущества использования RAII с интеллектуальными указателями?

6 ответов

Решение

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

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

это умный указатель, который владеет исключительно объектом. Это не в поддержку, но, вероятно, появится в следующем стандарте 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, владеющего ресурсом.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам