Как правильно инициализировать объект. [C ++] - PullRequest
5 голосов
/ 19 декабря 2009

Я упомянул в одном из моих предыдущих вопросов, что я читаю книгу Херба Саттера и Андрея Александреску "Стандарты кодирования C ++". В одной из глав они говорят что-то вроде этого:

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

Означает ли это, что я должен использовать конструкцию этой формы (при условии, что data_3_ должен быть инициализирован с новым):

SomeClass(const T& value, const U& value2, const R& value3)
    : data_(value), data_2_(value2)
{
    data_3_ = new value3;
}

вместо:

SomeClass(const T& value, const U& value2, const R& value3)
    : data_(value), data_2_(value2), data_3_(new value3)
    // here data_3_ is initialized in ctor initialization list
    // as far as I understand that incorrect way according to authors
{
}  

Заранее спасибо.

P.S. И если это то, что они имеют в виду, почему они используют термин неуправляемое приобретение ресурсов? Я всегда думал, что эти ресурсы "управляются вручную"?

P.S. 2. Заранее прошу прощения, если есть какие-либо проблемы с форматированием в этом посте - я должен признать - я абсолютно не люблю способ форматирования на этом форуме.

Ответы [ 5 ]

10 голосов
/ 19 декабря 2009

Совет необходим, если класс содержит два или более неуправляемых ресурса. Если выделение одного не удастся, вам нужно будет освободить все предыдущие выделенные ресурсы, чтобы избежать утечки. (РЕДАКТИРОВАТЬ: в более общем случае любое исключение, выданное после выделения ресурса, должно обрабатываться путем удаления этого ресурса). Это невозможно сделать, если они размещены в списке инициализатора. Например:

SomeClass() : data1(new value1), data2(new value2) {}

будет пропускать value1, если new value2 бросит. Вам нужно будет справиться с этим, вот так:

SomeClass() : data1(0), data2(0)
{
    data1 = new value1; // could be in the initialiser list if you want
    try
    {
        data2 = new value2;
    }
    catch (...)
    {
        delete data1;
        throw;
    }
}

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

9 голосов
/ 19 декабря 2009

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

Сначала рассмотрим этот код с автоматически управляемыми ресурсами:

class Breakfast {
public:
    Breakfast()
        : spam(new Spam)
        , sausage(new Sausage)
        , eggs(new Eggs)
    {}

    ~Breakfast() {}
private:
    // Automatically managed resources.
    boost::shared_ptr<Spam> spam;
    boost::shared_ptr<Sausage> sausage;
    boost::shared_ptr<Eggs> eggs;
};

Если "new Eggs" throws, ~Breakfast не вызывается, но деструкторы всех созданных членов вызываются в обратном порядке, то есть деструкторы sausage и spam.

Все ресурсы освобождены должным образом, здесь нет проблем.

Если вы используете необработанные указатели (управляемые вручную):

class Breakfast {
public:
    Breakfast()
        : spam(new Spam)
        , sausage(new Sausage)
        , eggs(new Eggs)
    {}

    ~Breakfast() {
        delete eggs;
        delete sausage;
        delete spam;
    }
private:
    // Manually managed resources.
    Spam *spam;
    Sausage *sausage;
    Eggs *eggs;
};

Если "new Eggs" throws, помните, ~Breakfast не вызывается, а скорее деструкторы spam и sausage (которые ничего не значат в этой причине, потому что у нас есть необработанные указатели в качестве реальных объектов).

Поэтому у вас есть утечка.

Правильный способ переписать приведенный выше код таков:

class Breakfast {
public:
    Breakfast()
        : spam(NULL)
        , sausage(NULL)
        , eggs(NULL)
    {
        try {
            spam = new Spam;
            sausage = new Sausage;
            eggs = new Eggs;
        } catch (...) {
            Cleanup();
            throw;
        }
    }

    ~Breakfast() {
        Cleanup();
    }
private:
    void Cleanup() {
        // OK to delete NULL pointers.
        delete eggs;
        delete sausage;
        delete spam;
    }

    // Manually managed resources.
    Spam *spam;
    Sausage *sausage;
    Eggs *eggs;
};

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

3 голосов
/ 19 декабря 2009

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

Рассмотрим:

A * a;
...
a = new A;

Что будет, если сработает конструктор А?

  • во-первых, конструируемый экземпляр A никогда не создается полностью
  • в таком случае деструктор А не будет вызван - на самом деле объекта А нет
  • память, выделенная новым для построения A, освобождена
  • происходит раскрутка стека, это означает, что присвоение указателю 'a' никогда не происходит, оно сохраняет свое первоначальное неопределенное значение.

Из этого должно быть очевидно, что нет ничего, что вызывало бы удаление, нет выделенной памяти, нет объекта типа A. Та же логика сохраняется, если новые броски, кроме конструктора A, никогда не использовались бы в первую очередь.

3 голосов
/ 19 декабря 2009

Это потому, что конструктор SomeClass может выдать исключение.

В описываемой вами ситуации (т.е. без использования умного указателя) вы должны освободить ресурс в деструкторе И, если конструктор SomeClass выдает исключения, с блоком try-catch:

SomeClass(const T& value, const U& value2, const R& value3):data_(value),data_2_(value2) :
data_3_(NULL)
{
    try 
    {
        data_3_ = new value3;

        // more code here that may throw an exception
    }
    catch(...)
    {
        delete data_3_;
        throw;
    }
}

.. Что вы не можете сделать, если в списке инициализации выдается исключение.

См. это для дальнейших объяснений.

1 голос
/ 19 декабря 2009

Это вопрос исключительной безопасности.

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

Давайте рассмотрим:

SomeClass() : data1(new T1()), data2(new T2()), data3(new T3()) {}

Если сгенерирует конструктор T2 или T3, вы определенно потеряете память, соответствующую инициализации data1. Кроме того, вы не будете знать, какое распределение вызвало исключение: это было new T2() или new T3()? и это такой случай, когда вы не знаете, безопасно ли это delete data2; как часть обработчика исключений конструктора.

Чтобы написать безопасный код исключения, используйте умные указатели или используйте блоки try / catch в теле конструктора.

SomeClass() : data1(new T1()), data2(new T2()), data3(new T3())
{
  data1 = new T1();
  try
  {
    data2 = new T2();
    try
    {
      data3 = new T3();
    }
    catch (std::exception&)
    {
      delete data2;
      throw;
    }
  }
  catch (std::exception&)
  {
    delete data1;
    throw;
  }
}

Как вы можете видеть, использование блоков try / catch не так читабельно и, как правило, подвержено ошибкам по сравнению с использованием интеллектуальных указателей элементов.

Примечание. Глава 48 «Стандарты кодирования C ++» относится к пункту 18 «Более исключительный C ++», который сам относится к разделу 16.5 Страуструпа «Разработка и развитие C ++ 3» и Разделу 14.4 Страуструпа «Язык программирования C ++».

РЕДАКТИРОВАТЬ: «Больше исключительных C ++ Элемент 18 имеет то же содержание, что и ПОЛУЧИЛ # 66: Сбои конструктора . Если у вас нет книги, обратитесь к веб-странице.

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