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

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

189 голосов
/ 27 декабря 2008

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

Ответы [ 6 ]

311 голосов
/ 27 декабря 2008

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

139 голосов
/ 28 декабря 2008

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);
}

Как вы видите, есть много способов, которыми мы можем стать жертвами. Идея состоит в том, что мы инкапсулируем управление ресурсами в класс. Инициализация его объекта получает ресурс («Приобретение ресурса - Инициализация»). Когда мы выходим из блока (область видимости блока), ресурс снова освобождается.

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

32 голосов
/ 27 декабря 2008

Суть и причины просты в концепции.

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

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

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

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

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

8 голосов
/ 27 декабря 2008

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

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

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

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

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

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

2 голосов
/ 27 декабря 2008

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

0 голосов
/ 27 декабря 2008
void foo()
{
   std::string bar;
   //
   // more code here
   //
}

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

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

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

...