Понимание значения термина и концепции - RAII (Приобретение ресурсов - Инициализация) - PullRequest
106 голосов
/ 03 апреля 2009

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

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

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

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

Спасибо.

Ответы [ 11 ]

129 голосов
/ 03 апреля 2009

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

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

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

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


C ++ гарантирует, что деструктор будет вызываться тогда и только тогда, когда конструктор завершился успешно. Полагаясь на это, 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 для освобождения файлов. Но если был создан original, его деструктор будет вызываться во время очистки конструктора Logger.

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


Отрицательные баллы:

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

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

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

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


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

43 голосов
/ 20 января 2011

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

0. RAII о масштабах

RAII о обоих:

  1. получение ресурса (независимо от того, какой ресурс) в конструкторе и удаление его в деструкторе.
  2. с конструктором, выполняемым при объявлении переменной, и деструктором, автоматически выполняемым, когда переменная выходит из области видимости.

Другие уже ответили об этом, поэтому я не буду уточнять.

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

Мсье Журден: Что! Когда я говорю: «Николь, принеси мне мои тапочки, и дай мне мой колпак ", это проза?

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

Мсье Журден: Более сорока лет я говорил прозой, ничего не зная об этом, и я очень благодарен вам за то, что вы научили меня этому.

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

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

void foo()
{
   // etc.

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

   // etc.
}

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

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

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

В C ++ было бы написано:

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 / C # (с использованием макросов C ++):

void foo()
{
   // etc.

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

   // etc.
}

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

БЕЛЫЙ КРОЛИК: [поет] Я опаздываю / опаздываю / На очень важную дату. / Нет времени говорить "Привет". / Прощай. / Я опоздал, я опоздал, я опоздал.

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

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

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

void foo()
{
   double timeElapsed = 0 ;

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

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

void foo()
{
   double timeElapsed = 0 ;

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

3. Почему C ++ не хватает finally?

[КУРИТЬ] Это окончательный обратный отсчет!

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

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

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

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

Вывод: RAII - это больше, чем философия в C ++: это C ++

RAII? ЭТО С ++ !!!

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

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

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

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

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

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

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

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

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

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

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

SmartPointer pointer( new ObjectClass() );

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

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

8 голосов
/ 03 апреля 2009

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

RAII, Получение ресурсов - инициализация означает, что все полученные ресурсы должны быть получены в контексте инициализации объекта. Это запрещает «голое» приобретение ресурсов. Обоснование состоит в том, что очистка в C ++ работает на основе объекта, а не на основе вызова функции. Следовательно, вся очистка должна выполняться объектами, а не вызовами функций. В этом смысле C ++ более объектно-ориентирован, чем, например, Джава. Очистка Java основана на вызовах функций в предложениях finally.

8 голосов
/ 03 апреля 2009

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

Это называется Resource Acquisition Is Initialization, потому что ресурс получается, когда создается объект, управляющий ресурсом. Если конструктор вышел из строя (т.е. из-за исключения), ресурс не был получен. Затем, когда объект выходит из области видимости, ресурс освобождается. C ++ гарантирует, что все объекты в стеке, которые были успешно построены, будут разрушены (это включает в себя конструкторы базовых классов и членов, даже если конструктор суперкласса завершится неудачно).

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

7 голосов
/ 03 апреля 2009

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

4 голосов
/ 03 апреля 2009

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

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

3 голосов
/ 03 апреля 2009

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

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?

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

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

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

2 голосов
/ 03 апреля 2009

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

...