Понимание значения термина и концепции - RAII (Инициализация ресурсов - Инициализация)

Не могли бы вы разработчики С++ дать нам хорошее описание того, что такое RAII, почему это важно и может ли оно иметь какое-либо отношение к другим языкам?

Я знаю немного. Я считаю, что это означает "Инициализация ресурсов". Однако это имя не преувеличивает мое (возможно неправильное) понимание того, что представляет собой RAII: создается впечатление, что RAII - это способ инициализации объектов в стеке, так что, когда эти переменные выходят из сферы действия, деструкторы будут автоматически называть причину очистки ресурсов.

Так почему же это не называется "использование стека для запуска очистки" (UTSTTC:)? Как вы добираетесь оттуда до "RAII"?

И как вы можете что-то сделать в стеке, что приведет к очистке чего-то, что живет в куче? Кроме того, существуют случаи, когда вы не можете использовать RAII? Вы когда-нибудь мечтали о сборке мусора? По крайней мере, сборщик мусора, который вы можете использовать для некоторых объектов, позволяя другим управлять?

Спасибо.

Ответ 1

Так почему же это не называется "использование стека для запуска очистки" (UTSTTC:)?

RAII сообщает вам, что делать: приобретайте свой ресурс в конструкторе! Я бы добавил: один ресурс, один конструктор. UTSTTC - это всего лишь одно приложение, RAII намного больше.

Управление ресурсами отстой. Здесь ресурс - это что-то, что требует очистки после использования. Исследования проектов на многих платформах показывают, что большинство ошибок связаны с управлением ресурсами - и это особенно плохо для Windows (из-за множества типов объектов и распределителей).

В С++ управление ресурсами особенно сложно из-за комбинации исключений и шаблонов (С++ style). Для просмотра под капотом см. GOTW8).


С++ гарантирует, что деструктор называется , если и только если конструктор сработал. Опираясь на это, RAII может решить множество неприятных проблем, о которых может даже не знать средний программист. Вот несколько примеров за пределами "мои локальные переменные будут уничтожены всякий раз, когда я вернусь".

Начнем с чрезмерно упрощенного класса FileHandle с использованием RAII:

class FileHandle
{
    FILE* file;

public:

    explicit FileHandle(const char* name)
    {
        file = fopen(name);
        if (!file)
        {
            throw "MAYDAY! MAYDAY";
        }
    }

    ~FileHandle()
    {
        // The only reason we are checking the file pointer for validity
        // is because it might have been moved (see below).
        // It is NOT needed to check against a failed constructor,
        // because the destructor is NEVER executed when the constructor fails!
        if (file)
        {
            fclose(file);
        }
    }

    // The following technicalities can be skipped on the first read.
    // They are not crucial to understanding the basic idea of RAII.
    // However, if you plan to implement your own RAII classes,
    // it is absolutely essential that you read on :)



    // It does not make sense to copy a file handle,
    // hence we disallow the otherwise implicitly generated copy operations.

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;



    // The following operations enable transfer of ownership
    // and require compiler support for rvalue references, a C++0x feature.
    // Essentially, a resource is "moved" from one object to another.

    FileHandle(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
    }

    FileHandle& operator=(FileHandle&& that)
    {
        file = that.file;
        that.file = 0;
        return *this;
    }
}

Если конструкцию не удается (за исключением), никакая другая функция-член - даже деструктор - не вызвана.

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

Теперь давайте посмотрим на временные объекты:

void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
    CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

Существует три случая ошибок: ни один файл не может быть открыт, можно открыть только один файл, оба файла можно открыть, но копирование файлов не удалось. В реализации, отличной от RAII, Foo должно было бы обрабатывать все три случая явно.

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

Теперь, объединим некоторые объекты:

class Logger
{
    FileHandle original, duplex;   // this logger can write to two files at once!

public:

    Logger(const char* filename1, const char* filename2)
    : original(filename1), duplex(filename2)
    {
        if (!filewrite_duplex(original, duplex, "New Session"))
            throw "Ugh damn!";
    }
}

Конструктор Logger завершится с ошибкой, если конструктор original завершится с ошибкой (поскольку filename1 не может быть открыт), конструктор duplex не работает (поскольку filename2 не может быть открыт) или записи в файлы внутри тело конструктора Logger не работает. В любом из этих случаев деструктор Logger не будет вызываться - поэтому мы не можем полагаться на Logger destructor для выпуска файлов. Но если был построен original, его деструктор будет вызван во время очистки конструктора Logger.

RAII упрощает очистку после частичной конструкции.


Отрицательные точки:

Отрицательные моменты? Все проблемы могут быть решены с помощью RAII и интеллектуальных указателей;-)

RAII иногда громоздка, когда вам требуется отсроченное получение, нажатие агрегированных объектов на кучу.
Представьте себе, что Logger нуждается в SetTargetFile(const char* target). В этом случае дескриптор, который все еще должен быть членом Logger, должен находиться в куче (например, в интеллектуальном указателе, чтобы соответствующим образом вызвать уничтожение дескриптора).

Я никогда не желал сбор мусора. Когда я делаю С#, я иногда чувствую момент блаженства, что мне просто не нужно заботиться, но гораздо больше я пропускаю все классные игрушки, которые могут быть созданы через детерминированное разрушение. (использование IDisposable просто не разрезает его.)

У меня была одна особенно сложная структура, которая могла бы извлечь выгоду из GC, где "простые" интеллектуальные указатели вызывали бы циклические ссылки по нескольким классам. Мы путались, тщательно балансируя сильные и слабые указатели, но в любое время, когда мы хотим что-то изменить, нам нужно изучить большую диаграмму отношений. Возможно, GC был лучше, но некоторые компоненты содержали ресурсы, которые следует выпускать как можно скорее.


Примечание к образцу FileHandle: оно не предназначалось для заполнения, просто для примера, но оказалось неправильным. Спасибо Johannes Schaub за указание и FredOverflow за то, что превратили его в правильное решение С++ 0x. Со временем я решил использовать здесь.

Ответ 2

Есть отличные ответы, поэтому я просто добавляю некоторые вещи, забытые.

0. RAII относится к областям

RAII относится к обоим:

  • получение ресурса (независимо от того, какой ресурс) в конструкторе, и un-получение его в деструкторе.
  • имеющий конструктор, выполняемый при объявлении переменной, и деструктор автоматически выполняется, когда переменная выходит за пределы области.
Другие говорили об этом, поэтому я не буду подробно останавливаться.

1. При кодировании на Java или С# вы уже используете RAII...

МОНСИУР ЮУРДАИН: Что! Когда я говорю: "Николь, принеси мне мои тапочки, и дай мне свой ночной колпак", эта проза?

ФИЛОСОФИЯ МАСТЕР: Да, сэр.

MONSIEUR JOURDAIN: На протяжении более сорока лет я говорил прозу, ничего не зная об этом, и я очень благодарен вам за то, что вы мне это научили.

- Мольер: джентльмен среднего класса, закон 2, сцена 4

Как сказал г-н Журден с прозой, С# и даже люди Java уже используют RAII, но скрытыми способами. Например, следующий код Java (который написан таким же образом в С#, заменив synchronized на lock):

void foo()
{
   // etc.

   synchronized(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

... уже использует RAII: сбор мьютекса выполняется в ключевом слове (synchronized или lock), а un-получение будет выполняться при выходе из области.

Это настолько естественно, что в его обозначениях почти нет объяснений даже для людей, которые никогда не слышали об RAII.

Преимущество С++ над Java и С# заключается в том, что с помощью RAII можно сделать что угодно. Например, в С++ нет прямого встроенного эквивалента synchronized и lock, но мы все еще можем их использовать.

В С++ это было бы написано:

void foo()
{
   // etc.

   {
      Lock lock(someObject) ; // lock is an object of type Lock whose
                              // constructor acquires a mutex on
                              // someObject and whose destructor will
                              // un-acquire it 

      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

который может быть легко написан способом Java/С# (с использованием макросов С++):

void foo()
{
   // etc.

   LOCK(someObject)
   {
      // if something throws here, the lock on someObject will
      // be unlocked
   }

   // etc.
}

2. RAII имеют альтернативные варианты использования

БЕЛЫЙ КРЫШКА: [пение] Я опаздываю/опаздываю/на очень важную дату./Нет времени, чтобы сказать "Привет"./Прощай./Я опаздываю, опаздываю, опаздываю.

- Алиса в Стране Чудес (версия Диснея, 1951)

Вы знаете, когда будет вызван конструктор (в объявлении объекта), и вы знаете, когда его соответствующий деструктор будет вызываться (при выходе из области), так что вы можете написать почти магический код с помощью строки. Добро пожаловать в С++ wonderland (по крайней мере, с точки зрения разработчика С++).

Например, вы можете написать объект-счетчик (я разрешаю это как упражнение) и использовать его, просто объявив его переменную, например, как объект блокировки, указанный выше:

void foo()
{
   double timeElapsed = 0 ;

   {
      Counter counter(timeElapsed) ;
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter declaration till the scope exit
}

который, конечно же, может быть записан снова с помощью Java/С# с использованием макроса:

void foo()
{
   double timeElapsed = 0 ;

   COUNTER(timeElapsed)
   {
      // do something lengthy
   }
   // now, the timeElapsed variable contain the time elapsed
   // from the Counter declaration till the scope exit
}

3. Почему С++ отсутствует finally?

[SHOUTING] Это обратный отсчет final!

- Европа: последний отсчет (извините, я был вне цитат, здесь...: -)

Предложение finally используется в С#/Java для обработки ресурсов в случае выхода области видимости (либо через return, либо заброшенное исключение).

Астуальные читатели спецификации заметили, что С++ не имеет окончательного предложения. И это не ошибка, потому что С++ не нуждается в ней, поскольку RAII уже обрабатывает удаление ресурсов. (И поверьте мне, писать деструктор С++ легче, чем писать правильное предложение Java finally, или даже С# метод Dispose).

Тем не менее, иногда предложение finally было бы круто. Можем ли мы это сделать на С++? Да, мы можем! И снова с альтернативным использованием RAII.

Заключение: RAII - это больше, чем философия в С++: это С++

RAII? ЭТО C++!!!

- разработчик С++ возмутил комментарий, бесстыдно скопированный неясным королем Спарты и его 300 друзьями

Когда вы достигаете некоторого уровня опыта работы на С++, вы начинаете думать в терминах RAII, в терминах автоматического запуска подрядчиков и деструкторов.

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

И почти все подходит в смысле RAII: безопасность исключений, мьютексы, соединения с базами данных, запросы к базе данных, соединение с сервером, часы, дескрипторы ОС и т.д., и последняя, ​​но не менее важная память.

Часть базы данных не является незначительной, так как, если вы принимаете оплату цены, вы даже можете записать транзакционное программирование "стиль, выполнение строк и строк кода до принятия решения, в конце концов, если вы хотите зафиксировать все изменения или, если это невозможно, вернуть все изменения назад (если каждая строка удовлетворяет хотя бы Сильной гарантии исключения). (см. вторую часть этой статьи Herb Sutter для транзакционного программирования).

И, как головоломка, все подходит.

RAII - это большая часть С++, С++ не может быть С++ без него.

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

И это объясняет, почему сборщик мусора, в то время как великолепная часть технологии сама по себе не так впечатляет с точки зрения разработчика С++:

  • RAII уже обрабатывает большинство случаев, обрабатываемых GC
  • GC работает лучше, чем RAII с круговыми ссылками на чистые управляемые объекты (смягчается интеллектуальным использованием слабых указателей)
  • Тем не менее GC ограничен памятью, а RAII может обрабатывать любые ресурсы.
  • Как описано выше, RAII может сделать многое, многое другое...

Ответ 4

RAII использует семантику деструкторов С++ для управления ресурсами. Например, рассмотрим умный указатель. У вас есть параметризованный конструктор указателя, который инициализирует этот указатель с помощью адреса объекта. Вы выделяете указатель на стек:

SmartPointer pointer( new ObjectClass() );

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

Есть случаи, когда RAII не помогает. Например, если вы используете интеллектуальные указатели с подсчетом ссылок (например, boost:: shared_ptr) и создаете структуру, подобную графу, с циклом, с которым вы рискуете столкнуться с утечкой памяти, потому что объекты в цикле будут препятствовать освобождению друг от друга. Сбор мусора поможет в этом.

Ответ 5

Я согласен с cpitis. Но хотелось бы добавить, что ресурсы могут быть чем угодно, а не только памятью. Ресурсом может быть файл, критический раздел, поток или соединение с базой данных.

Он называется инициализацией ресурсов, потому что ресурс получен, когда объект, управляющий ресурсом, сконструирован. Если конструктор не прошел (т.е. из-за исключения), ресурс не был получен. Затем, когда объект выходит из области действия, ресурс освобождается. С++ гарантирует, что все объекты в стеке, которые были успешно созданы, будут разрушены (сюда входят конструкторы базовых классов и членов, даже если конструктор суперкласса не работает).

Рациональное значение RAII заключается в том, чтобы сделать исключение извлечения ресурсов безопасным. То, что все приобретенные ресурсы должным образом выпущены независимо от того, где происходит исключение. Однако это зависит от качества класса, который приобретает ресурс (это должно быть безопасным исключением, и это сложно).

Ответ 6

Я бы хотел сказать немного сильнее, чем предыдущие ответы.

RAII, Resource Assquisition Is Is Initialization означает, что все приобретенные ресурсы должны быть приобретены в контексте инициализации объекта. Это запрещает "голый" сбор ресурсов. Обоснованием является то, что очистка в С++ работает на основе объекта, а не на основе функционального вызова. Следовательно, вся очистка должна выполняться объектами, а не вызовами функций. В этом смысле С++ более объектно ориентирован, то есть, например, Ява. Очистка Java основана на вызовах функций в предложениях finally.

Ответ 7

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

Ответ 8

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

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

Ответ 9

И как вы можете что-то сделать в стеке, что приведет к очистке чего-то, что живет в куче?

class int_buffer
{
   size_t m_size;
   int *  m_buf;

   public:
   int_buffer( size_t size )
     : m_size( size ), m_buf( 0 )
   {
       if( m_size > 0 )
           m_buf = new int[m_size]; // will throw on failure by default
   }
   ~int_buffer()
   {
       delete[] m_buf;
   }
   /* ...rest of class implementation...*/

};


void foo() 
{
    int_buffer ib(20); // creates a buffer of 20 bytes
    std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.

Когда возникает экземпляр int_buffer, он должен иметь размер, и он будет выделять необходимую память. Когда он выходит из сферы действия, он вызывается деструктором. Это очень полезно для таких объектов, как объекты синхронизации. Рассмотрим

class mutex
{
   // ...
   take();
   release();

   class mutex::sentry
   {
      mutex & mm;
      public:
      sentry( mutex & m ) : mm(m) 
      {
          mm.take();
      }
      ~sentry()
      {
          mm.release();
      }
   }; // mutex::sentry;
};
mutex m;

int getSomeValue()
{
    mutex::sentry ms( m ); // blocks here until the mutex is taken
    return 0;  
} // the mutex is released in the destructor call here.

Также есть случаи, когда вы не можете использовать RAII?

Нет, не совсем.

Вы когда-нибудь находите себе желание собирать мусор? По крайней мере, сборщик мусора, который вы можете использовать для некоторых объектов, позволяя другим управлять?

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

Ответ 10

Здесь уже много хороших ответов, но я просто хотел бы добавить:
Простое объяснение RAII заключается в том, что в С++ объект, выделенный в стеке, уничтожается всякий раз, когда он выходит за пределы области видимости. Это означает, что вызовет деструктор объектов и может выполнить всю необходимую очистку.
Это означает, что если объект создается без "нового", не требуется "удалить". И это также идея "умных указателей" - они находятся в стеке и по существу обертывают объект, основанный на куче.

Ответ 11

RAII - это аббревиатура для инициализации ресурсов.

Эта методика очень уникальна для С++ из-за их поддержки как конструкторов, так и деструкторов и почти автоматически конструкторов, которые сопоставляют передаваемые аргументы или наихудший случай, который вызывается конструктором по умолчанию, и деструкторы, если предоставленная объяснение называется иначе по умолчанию, который добавляется компилятором С++, вызывается, если вы явно не указали деструктор для класса С++. Это происходит только для объектов С++, которые автоматически управляются - это означает, что они не используют свободный магазин (выделенная память/освобожденная память с использованием новых, новых []/delete, delete [] операторов С++).

Метод RAII использует эту функцию автоматического управления объектом для обработки объектов, созданных в куче/свободном хранилище, путем детального запроса дополнительной памяти с помощью new/new [], которые должны быть явно уничтожены вызовом delete/Удалить[]. Класс объекта с автоматическим управлением обернет этот другой объект, который создается в памяти кучи/свободного хранилища. Следовательно, когда запускается конструктор с автоматическим управлением, обернутый объект создается в памяти кучи/свободного хранилища, и когда дескриптор объекта с автоматическим управлением выходит из области видимости, деструктор этого автоматически управляемого объекта вызывается автоматически, в котором завернутый объект уничтожается с помощью delete. С концепциями ООП, если вы обернете такие объекты внутри другого класса в частной области, у вас не будет доступа к членам и методам обернутых классов, и именно по этой причине разработаны интеллектуальные указатели (например, классы дескрипторов). Эти умные указатели выставляют обернутый объект как типизированный объект во внешний мир и там, позволяя вызывать любые элементы/методы, из которых состоит объект открытой памяти. Обратите внимание, что интеллектуальные указатели имеют разные вкусы, основанные на разных потребностях. Для получения дополнительной информации об этом вам следует обратиться к программированию на современном языке С++ от Andrei Alexandrescu или дополнительной библиотеке (www.boostorg) shared_ptr.hpp. Надеюсь, это поможет вам понять RAII.