Как правильно инициализировать объект. [C++]

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

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

Делает это означает, что я должен использовать конструкцию этой формы (если это 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
задан Alex B 19 December 2009 в 15:17
поделиться

5 ответов

Это потому, что конструктор 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;
    }
}

... Что нельзя сделать, если в списке инициализации брошено исключение.

Смотрите this для дальнейших пояснений.

.
3
ответ дан 18 December 2019 в 06:11
поделиться

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

Рассмотрим:

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

Что произойдет, если бросит конструктор A?

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

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

.
3
ответ дан 18 December 2019 в 06:11
поделиться

Совет необходим, если класс содержит два или более неуправляемых ресурса. Если выделение одного не удается, то во избежание утечки необходимо освободить все ранее выделенные ресурсы. (EDIT: в более общем случае, любое исключение, брошенное после выделения ресурса, должно быть обработано путем удаления этого ресурса). Это невозможно сделать, если они распределены в списке инициализаторов. Например:

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

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

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

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

.
10
ответ дан 18 December 2019 в 06:11
поделиться

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

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

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" , то не вызывается ~Breakfast, но вызываются деструкторы всех членов конструкции в обратном порядке, то есть деструкторы колбасы и спама .

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

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

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

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

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

Правильным способом переписывания кода выше является следующее:

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 класс, чтобы вы могли управлять им автоматически и группировать его в другие классы.

.
9
ответ дан 18 December 2019 в 06:11
поделиться

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

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

Рассмотрим:

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

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

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

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

EDIT: "Более исключительный C++ пункт 18 имеет то же содержание, что и GotW #66: Неисправности конструктора". Обратитесь к веб-странице, если у вас нет книги.

.
1
ответ дан 18 December 2019 в 06:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: