Подтвердить что ты не робот

RAII и интеллектуальные указатели в С++

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

4b9b3361

Ответ 1

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

Ответ 2

RAII Это странное имя для простой, но удивительной концепции. Лучше это имя Ограниченное управление ресурсами (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

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

код:

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 может быть помещен в контейнер, потому что контейнеры смогут хранить не скопируемые (но перемещаемые) типы, такие как потоки и 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. Он будет генерировать исключение, когда он замечает, что вы пытаетесь получить доступ к ресурсу по пути доступа weak_ptr, даже несмотря на то, что shared_ptr больше не владеет ресурсом.

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

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

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

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

Источники интеллектуальных указателей на С++ находятся в миллионах вокруг сети, включая ответы выше меня.

Ответ 5

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

Ответ 6

void foo()
{
   std::string bar;
   //
   // more code here
   //
}

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

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

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