Исключения и конструкторы

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

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

Спецификатор noexcept и члены класса

Начнем с вопроса, который какое-то время не давал мне покоя. Рассмотрим 2 простых класса.

struct MySubStruct
{
    MySubStruct()
    {
        //do something
    }
};

class MyStruct
{
public:
    explicit MyStruct(int i) : m_int(i)
    {}

private:
    int m_int;
    MySubStruct m_sub_struct;
};

Конструктор класса MyStruct по сути ничего не делает. Визуально у него пустое тело. Можем ли мы добавить к нему спецификатор noexcept?

Наивный взгляд подсказывает что да, можем. Ведь чисто визуально конструктор этого класса ничего не делает. А значит и бросить исключение вроде как не может.

Но это ведь С++. А он наивность не прощает.

На самом деле конструктор класса MyStruct не пуст. Помимо своего тела (которое действительно ничего не делает) он включает в себя конструирование полей m_int и m_sub_struct. Полностью он может выглядеть так (псевдокод):

MyStruct::MyStruct(int i)
{
    construct(&m_int, i);
    construct(&m_sub_struct); //!!!!
    call_constructor_body();
}

Так как конструктор класса MySubStruct не имеет спецификатора noexcept, он может бросить исключение, которое выйдет за пределы конструктора класса MyStruct. Если у него есть спецификатор noexcept, то это приведет к вызову std::terminate и аварийному завершению программы.

Эта ситуация демонстрируется в примере ниже.

#include <iostream>

struct MySubStruct
{
    MySubStruct()
    {
        throw std::runtime_error{"exception"};
    }
};

class MyStruct
{
public:
    explicit MyStruct(int i) noexcept : m_int(i)
    {}

private:
    int m_int;
    MySubStruct m_sub_struct;
};

int main() 
{
    try
    {
        MyStruct ms{42};
    }
    catch(const std::exception &e)
    {
        std::cerr << "Exception: " << e.what() << '\n';
    }

    return 0;
};

Как тогда правильно записать спецификатор noexcept? Полностью отказываться от него не хочется. Постоянно следить за членами класса и их конструкторами — слишком трудоёмко и чревато ошибками.

К счастью есть выход с помощью std::is_nothrow_constructible<> (появился в С++11) и std::is_nothrow_constructible_v<> (появился в С++17) [1]. С этими метафункциями пример выше примет вид:

#include <iostream>
#include <type_traits>

struct MySubStruct
{
    MySubStruct()
    {
        throw std::runtime_error{"exception"};
    }
};

class MyStruct
{
public:
    explicit MyStruct(int i)
        noexcept(std::is_nothrow_constructible_v<MySubStruct>)
    : m_int(i)
    {}

private:
    int m_int;
    MySubStruct m_sub_struct;
};

int main() 
{
    try
    {
        MyStruct ms{42};
    }
    catch(const std::exception &e)
    {
        std::cerr << "Exception: " << e.what() << '\n';
    }

    return 0;
};

В этом примере конструктор класса MyStruct будет иметь спецификатор noexcept только в том случае, если конструктор класса MySubStruct так же имеет этот спецификатор. Поскольку теперь спецификатор устанавливается правильно, брошенное исключение легально покидает конструктор класса MyStruct и перехватывается в функции main.

Нужно сделать несколько важных замечаний.

  1. Всё вышеописанное относится только к нестатическим членам класса. Статические члены конструируются вне конструктора класса.
  2. Конструктор класса включает в себя не только конструкторы его нестатических членов, но и конструкторы его родителей (если они есть).
  3. Как будет показано в следующем разделе, аналогичная логика применяется и для конструкторов копирования (перемещения).
  4. При использовании делегирующих конструкторов один конструктор может вызывать другие конструкторы. Тогда они тоже входят в его тело и влияют на спецификатор noexcept.

Спецификатор noexcept и конструкторы

Описанная в предыдущем разделе логика автоматически используется компилятором в том случае, когда он отвечает за создание конструктора класса. Такое происходит в одном из двух случаев: мы не объявили конструктор, или мы объявили его как default.

Ниже приводится пример программы, демонстрирующей автоматическое определение спецификатора noexcept для конструкторов компилятором для различных классов.

Для проверки наличия спецификатора noexcept используются метафункции:

  • std::is_nothrow_constructible_v<> — конструктор по умолчанию [1],
  • std::is_nothrow_copy_constructible_v<> — конструктор копирования [2],
  • std::is_nothrow_move_constructible_v<> — конструктор перемещения [3].
#include <string>
#include <type_traits>

class A {};

static_assert(std::is_nothrow_constructible_v<A>);
static_assert(std::is_nothrow_copy_constructible_v<A>);
static_assert(std::is_nothrow_move_constructible_v<A>);

//-------------------------------------------------

struct B
{
    int m_i;
    double m_d;
};

static_assert(std::is_nothrow_constructible_v<B>);
static_assert(std::is_nothrow_copy_constructible_v<B>);
static_assert(std::is_nothrow_move_constructible_v<B>);

//-------------------------------------------------

struct C
{
    std::string m_str;
};

static_assert(std::is_nothrow_constructible_v<C>);
static_assert(! std::is_nothrow_copy_constructible_v<C>);  //!
static_assert(std::is_nothrow_move_constructible_v<C>);

//-------------------------------------------------

struct D
{
    D() {}
};

static_assert(! std::is_nothrow_constructible_v<D>);     //!
static_assert(std::is_nothrow_copy_constructible_v<D>);
static_assert(std::is_nothrow_move_constructible_v<D>);

//-------------------------------------------------

struct E
{
    E() noexcept {}
};

static_assert(std::is_nothrow_constructible_v<E>);
static_assert(std::is_nothrow_copy_constructible_v<E>);
static_assert(std::is_nothrow_move_constructible_v<E>);

//-------------------------------------------------

struct FE : public E
{

};

static_assert(std::is_nothrow_constructible_v<FE>);
static_assert(std::is_nothrow_copy_constructible_v<FE>);
static_assert(std::is_nothrow_move_constructible_v<FE>);

//-------------------------------------------------

struct FD : public D
{

};

static_assert(! std::is_nothrow_constructible_v<FD>);  //!
static_assert(std::is_nothrow_copy_constructible_v<FD>);
static_assert(std::is_nothrow_move_constructible_v<FD>);

//-------------------------------------------------

int main()
{
    return 0;
}

Рассмотрим эти случаи более подробно.

Класс A не имеет ни членов, ни родителей. Поэтому все его конструкторы помечаются как noexcept.

Класс B имеет два поля, имеющих простые встроенные типы int и double. Конструирование, копирование и перемещение экземпляров этих типов никогда не бросает исключений. Поэтому все конструкторы класса B помечаются как noexcept.

Класс C имеет поле типа std::string. Поэтому конструкторы класса C зависят от соответствующих конструкторов класса std::string [4].

Конструктор копирования класса std::string не имеет спецификатора noexcept, так как он может бросить исключение, например при нехватке памяти. Поэтому и конструктор копирования класса C также не имеет этого спецификатора.

Конструктор перемещения класса std::string имеет спецификатор noexcept, поэтому и конструктор перемещения класса C имеет этот спецификатор.

С конструктором по умолчанию класса std::string всё намного интереснее [5]. По стандарту [4] до С++17 он не имеет спецификатора noexcept. Начиная с C++17 наличие этого спецификатора явно зависит от его наличия в конструкторе аллокатора, используемого в std::string. На деле же все тестируемые мной компиляторы (GCC, Clang и MSVC) ведут себя так, как это описано в С++17 даже если явно задан более старый стандарт.

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

В сухом остатке получаем. Так как конструктор аллокатора помечен noexcept [6], конструктор std::string так же помечается noexcept. А значит и конструктор класса C помечается noexcept.

Класс D не имеет ни членов, ни предков, но имеет явно заданный конструктор по умолчанию без спецификатора noexcept. Из-за этого метафункция std::is_nothrow_constructible возвращает false.

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

Класс E не имеет ни членов, ни предков, но имеет явно заданный конструктор по умолчанию со спецификатором noexcept. Поэтому все его конструкторы так же имеют этот спецификатор.

Класс FE не имеет ни членов, ни явно объявленных конструкторов, но он наследует от класса E. Поэтому спецификаторы всех его конструкторов так же наследуются от соответствующих конструкторов класса E.

Ситуация с классом FD аналогична ситуации с классом FE. Все спецификаторы наследуются от класса D.

Константные члены

Рассмотрим случай класса, имеющего константный член данных.

#include <string>
#include <type_traits>

struct A
{
    std::string m_str;
};

struct B
{
    const std::string m_str;
};

static_assert(std::is_nothrow_constructible_v<A>);
static_assert(! std::is_nothrow_copy_constructible_v<A>);
static_assert(std::is_nothrow_move_constructible_v<A>);

static_assert(std::is_nothrow_constructible_v<B>);
static_assert(! std::is_nothrow_copy_constructible_v<B>);
static_assert(! std::is_nothrow_move_constructible_v<B>);

//-------------------------------------------------

int main()
{
    return 0;
}

Класс A аналогичен классу C из предыдущего примера. Класс B отличается от A только тем, что его член данных m_str объявлен константным. Такое вроде бы простое изменение привело к тому, что конструктор перемещения класса B потерял спецификатор noexcept. Теперь он может бросить ислючение. Но почему?

Причина в невозможности переместить константный объект. Чтобы переместить объект необходима возможность изменить его, а такой возможности нет. Поэтому в случае константных объектов происходит их копирование а не перемещение, даже если Вы явно пишете std::move. А поскольку копирование std::string может бросить исключение, то значит и конструктор перемещения класса B теперь может бросить исключение.

Статические члены

Сравним два класса A и B.

#include <string>
#include <type_traits>

struct A
{
    std::string m_str;
};

struct B
{
    static std::string m_str;
};

static_assert(std::is_nothrow_constructible_v<A>);
static_assert(! std::is_nothrow_copy_constructible_v<A>);
static_assert(std::is_nothrow_move_constructible_v<A>);

static_assert(std::is_nothrow_constructible_v<B>);
static_assert(std::is_nothrow_copy_constructible_v<B>);
static_assert(std::is_nothrow_move_constructible_v<B>);

//-------------------------------------------------
int main()
{
    return 0;
}

Единственная разница между ними в том, что в классе B единственное поле m_str является статическим, а в классе A — нет. Такое изменение привело к тому, что конструктор копирования класса B получил спецификатор noexcept.

Причина этого в том, что статические члены класса не входят в состав экземпляра класса. Они хранятся отдельно от него. А значит, они не конструируются при конструировании экземпляра класса, не копируются при копировании экземпляра класса и не перемещаются при перемещении экземпляра класса.

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

class C {};

Поэтому все конструкторы класса B и имеют спецификатор noexсept.

Спецификатор noexcept и default

В большинстве примеров из предыдущих разделов я не объявлял конструкторы, отдавая это на откуп компилятору. А что если использовать ключевое слово default? Изменит ли это что-либо? На самом деле нет. И пример ниже это подтверждает.

#include <type_traits>

struct A {};

struct B
{
    B() = default;
    B(const B & ) = default;
    B( B && ) = default;
};

static_assert(std::is_nothrow_constructible_v<A>);
static_assert(std::is_nothrow_copy_constructible_v<A>);
static_assert(std::is_nothrow_move_constructible_v<A>);

static_assert(std::is_nothrow_constructible_v<B>);
static_assert(std::is_nothrow_copy_constructible_v<B>);
static_assert(std::is_nothrow_move_constructible_v<B>);

int main()
{
    return 0;
}

Обратите внимание: конструкторы класса A мы никак не объявляем, а конструкторы класса B — объявляем с помощью default. При этом все конструкторы обоих классов автоматически помечаются спецификатором noexcept.

Посмотрим на другой пример.

#include <type_traits>

struct SubClass
{
    SubClass() {}
};

struct C
{
    SubClass m_sc;

    C() = default;
};

struct D
{
    SubClass m_sc;

    D() noexcept = default;
};

static_assert(! std::is_nothrow_constructible_v<C>);

static_assert(std::is_nothrow_constructible_v<D>);
//-------------------------------------------------

int main()
{
    return 0;
}

Конструктор по умолчанию класса SubClass не имеет спецификатора noexcept. Это значит, что потенциально он может бросить исключение.

Так как класс SubClass входит в состав класса C, а конструктор последнего мы объявляем через default, конструктор по умолчанию класса C также не имеет спецификатора noexcept. Это мы обсуждали ранее.

С классом D ситуация иная. В нём мы явно устанавливаем спецификатор noexcept для конструктора. И здесь на самом деле закладывается мина.

Если конструктор класса SubClass бросит исключение во время конструирования класса C, оно легально покинет конструктор класса C. Здесь нет никаких сюрпризов.

Но, если конструктор класса SubClass бросит исключение во время конструирования класса D произойдет вызов std::terminate и, как следствие, аварийное завершение всей программы. Это произойдет из-за того, что конструктор класса D имеет спецификатор noexcept.

Поэтому смешивать noexcept и default может быть очень опасно. Тем не менее в обсуждении [7] по этому вопросу говорится:

Explicitly writing constexpr and noexcept is still useful; it means «yell at me if it can’t be constexpr/noexcept«.

Или в переводе на русский:

Явное указание constexpr и noexcept тоже полезно; оно значит «накричи на меня, если конструктор не может быть constexpr/noexcept«

Перехват исключений из конструкторов членов класса

В C++ есть специальный синтаксис, позволяющий перехватывать исключения из конструкторов членов класса. Он называется function try block [8]. Ниже приводится пример программы, демонстрирующей его использование.

#include <exception>
#include <iostream>

struct SubClass
{
    explicit SubClass(int)
    {
        std::cout << "SubClass constructor\n";
        throw std::runtime_error{"SubClass exception"};
    }
};

struct MyClass
{
    SubClass m_sc;

    explicit MyClass(int i) try : m_sc(i)
    {
        std::cout << "MyClass constructor\n";
    }
    catch(const std::exception &e)
    {
        std::cout << "MyClass has catched exception: "
            << e.what() << '\n';
        //throw std::runtime_error{"MyClass exception"};
    }
};

//-------------------------------------------------

int main()
{
    try
    {
        MyClass mc{42};
    }
    catch(const std::exception &e)
    {
        std::cout << "main has catched exception: "
            << e.what() << '\n';
    }

    return 0;
}

В этом примере у нас есть класс SubClass, конструктор которого бросает исключение. Экземпляр этого класса входит в состав класса MyClass. В конструкторе MyClass используется function try block для перехвата исключения, брошенного конструктором SubClass.

Важно понимать, что function try block позволяет перехватывать исключение, но не позволяет полностью обработать его. Если блок catch завершается без запуска нового исключения (как в примере выше), перехваченное им исключение перезапускается [8]. Поэтому в приведенном выше примере мы получим такой вывод:

SubClass constructor
MyClass has catched exception: SubClass exception
main has catched exception: SubClass exception

Единственное что мы можем в таких условиях сделать, это залогировать ошибку и/или бросить новое исключение [9].

Деструктор

Отвлечемся от спецификатора noexcept и рассмотрим другой пример.

struct MyStruct
{
    int *m_p_int;

    MyStruct()
    {
        m_p_int = new int(42);
        // do something
    }

    ~MyStruct()
    {
        if(m_p_int)
            delete m_p_int;
    }
};

Давайте отбросим вопрос о том, что следовало бы использовать умный (интеллектуальный), а не сырой указатель. Если забыть про этот момент, что не так с этим классом? Да, он полностью бесполезен. Но он сведен к минимуму, чтобы подсветить проблему, о которой я и хотел бы поговорить.

Что произойдет, если в части //do something будет брошено исключение? Будет ли здесь утечка?

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

Подвох в том, что в этом случае деструктор не будет вызван [10]. И этому есть простое объяснение. Так как конструктор не отработал до конца, объект не создан, а значит разрушать нечего. Как можно разрушить то, что не создано?

Ниже приводится пример, наглядно демонстрирующий эту логику.

#include <exception>
#include <iostream>

struct MyStruct
{
    MyStruct()
    {
        std::cout << "I am constructor\n";
        throw std::runtime_error{"exception"};
    }

    ~MyStruct()
    {
        std::cout << "I am destructor\n";
    }
};

//-------------------------------------------------

int main()
{
    try
    {
        MyStruct ms;
    }
    catch(const std::exception &e)
    {
        std::cerr << "main has catched exception: "
            << e.what() << '\n';
    }

    return 0;
}

// Output:
// I am constructor
// main has catched exception: exception

Обратите внимание: в результирующем выводе нет строки "I am destructor". Это значит, что деструктор не вызывался.

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

Примеры

Посмотрим, как это правило работает для членов данных.

#include <exception>
#include <iostream>

struct SubClass
{
    SubClass()
    {
        std::cout << "I am constructor of SubClass\n";
    }

    ~SubClass()
    {
        std::cout << "I am destructor of SubClass\n";
    }
};

struct MyStruct
{
    SubClass m_cs;

    MyStruct()
    {
        std::cout << "I am constructor of MyStruct\n";
        throw std::runtime_error{"exception"};
    }

    ~MyStruct()
    {
        std::cout << "I am destructor of MyStruct\n";
    }
};

//-------------------------------------------------

int main()
{
    try
    {
        MyStruct ms;
    }
    catch(const std::exception &e)
    {
        std::cerr << "main has catched exception: "
            << e.what() << '\n';
    }

    return 0;
}

// Output
// I am constructor of SubClass
// I am constructor of MyStruct
// I am destructor of SubClass
// main has catched exception: exception

У нас есть два класса: SubClass и MyStruct. При этом экземпляр SubClass является членом MyStruct. Вот что происходит в этом примере.

  1. В строке 39 мы пытаемся создать экземпляр класса MyStruct.
  2. Так как экземпляр класса SubClass является членом класса MyStruct, для него вызывается конструктор. Он успешно отрабатывает.
  3. Других членов у класса MyStruct нет. Поэтому вызывается его конструктор.
  4. Конструктор класса MyStruct бросает исключение (строка 24).
  5. Так как конструктор класса MyStruct не отработал до конца, экземпляр этого класса не создан. Поэтому деструктор класса MyStruct не вызывается.
  6. У класса MyStruct есть член m_cs, который был успешно создан на шаге 2. Для него вызывается деструктор.
  7. Брошенное на шаге 4 исключение покидает конструктор класса MyStruct и перехватывается в функции main.

Похожая ситуация происходит и при наследовании.

#include <exception>
#include <iostream>

struct Base
{
    Base()
    {
        std::cout << "I am constructor of Base\n";
    }

    ~Base()
    {
        std::cout << "I am destructor of Base\n";
    }
};

struct MyStruct : public Base
{
    MyStruct() : Base()
    {
        std::cout << "I am constructor of MyStruct\n";
        throw std::runtime_error{"exception"};
    }

    ~MyStruct()
    {
        std::cout << "I am destructor of MyStruct\n";
    }
};

//-------------------------------------------------

int main()
{
    try
    {
        MyStruct ms;
    }
    catch(const std::exception &e)
    {
        std::cerr << "main has catched exception: "
            << e.what() << '\n';
    }

    return 0;
}

// Output
// I am constructor of Base
// I am constructor of MyStruct
// I am destructor of Base
// main has catched exception: exception

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

  1. В строке 37 мы пытаемся создать экземпляр класса MyStruct.
  2. У класса MyStruct есть родитель — класс Base. Поэтому вызывается конструктор класса Base. Он успешно отрабатывает.
  3. У класса MyStruct нет членов. Поэтому вызывается его конструктор.
  4. Конструктор класса MyStruct бросает исключение (строка 22).
  5. Так как конструктор класса MyStruct не отработал до конца, экземпляр этого класса не создан. Поэтому деструктор класса MyStruct не вызывается.
  6. У класса MyStruct есть родитель, экземпляр которого был успешно создан на шаге 2. Для него вызывается деструктор.
  7. Брошенное на шаге 4 исключение покидает конструктор класса MyStruct и перехватывается в функции main.

Делегирующий конструктор

В C++11 появился такой механизм, как делегирующие конструкторы (delegating constructors) [11]. Он позволяет вызвать конструктор класса из другого конструктора этого же класса. Если этот механизм используется, то он может повлиять на вызов деструктора класса.

#include <exception>
#include <iostream>

struct MyStruct
{
    MyStruct()     {
        std::cout << "I am constructor without parameter\n";
    }

    explicit MyStruct(int) : MyStruct()
    {
        std::cout << "I am constructor with parameter\n";
        throw std::runtime_error{"exception"};
    }

    ~MyStruct()
    {
        std::cout << "I am destructor\n";
    }
};

//-------------------------------------------------

int main()
{
    try
    {
        MyStruct ms{42};
    }
    catch(const std::exception &e)
    {
        std::cerr << "main has catched exception: "
            << e.what() << '\n';
    }

    return 0;
}

// Output
// I am constructor without parameter
// I am constructor with parameter
// I am destructor
// main has catched exception: exception

По выводу программы видно, что деструктор класса MyStruсt всё-таки был вызван. Но почему? Причина в том, что для создаваемого экземпляра класса успешно отработал конструктор без параметра. Поэтому экземпляр считается созданным. Да, второй конструктор (с параметром) бросил исключение, но он вызывается после первого конструктора (без параметра) и работает с уже созданным экземпляром класса.

Поэтому в правиле выше и говорится: «хотя бы один конструктор«.

new и освобождение памяти

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

class MyClass { /* ... */ };

MyClass* make_class()
{
   return new MyClass();
}

Будет ли здесь утечка памяти, если конструктор класса MyClass бросит исключение [12]? Ответ прост. Нет, не будет.

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

Но нужно добавить два уточнения.

  1. Если для класса MyClass перегружен оператор new, то поведение может отличаться от стандартного.
  2. Размещающий new (placement-new) не освобождает память, выделенную под объект. Так как он не знает, как эта память была выделена.

Заключение

В этой статьте я постарался осветить как можно больше аспектов использования исключений в конструкторах классов.

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

Во-вторых, мы рассмотрели на примерах, как компилятор расставляет спецификатор noexcept для конструкторов (включая конструкторы копирования и перемещения).

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

Деструктор вызывается только для тех объектов, для которых успешно отработал хотя бы один конструктор.

Также на примерах мы увидели, как это правило работает.

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

Если вдруг я что-то упустил — пишите, я дополню материал.

Ссылки

  1. Описание std::is_nothrow_constructible_v<>: https://en.cppreference.com/w/cpp/types/is_constructible.html
  2. Описание std::is_nothrow_copy_constructible_v<>: https://en.cppreference.com/w/cpp/types/is_copy_constructible.html
  3. Описание std::is_nothrow_move_constructible_v<>: https://en.cppreference.com/w/cpp/types/is_move_constructible.html
  4. Конструкторы класса std::basic_string: https://en.cppreference.com/w/cpp/string/basic_string/basic_string.html
  5. Обсуждение наличия спецификатора noexcept у конструктора по умолчанию для класса std::string: https://stackoverflow.com/questions/14421193/is-stdstrings-default-constructor-no-throw
  6. Конструктор стандартного аллокатора: https://en.cppreference.com/w/cpp/memory/allocator/allocator.html
  7. Обсуждение noexcept/constexpr и default: https://stackoverflow.com/questions/36161188/is-a-defaulted-constructor-assignment-noexcept-constexpr-by-default
  8. Описание function try block: https://en.cppreference.com/w/cpp/language/try.html#Function_try_block
  9. Обсуждение практической пользы function try block в конструкторе класса: https://ru.stackoverflow.com/questions/798029/%D0%9E%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B9-%D0%B2-%D0%BA%D0%BE%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D0%BE%D1%80%D0%B0%D1%85-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%B2
  10. Обсуждение исключений из конструкторов и вызова соответствующих деструкторов: https://stackoverflow.com/questions/10212842/what-destructors-are-run-when-the-constructor-throws-an-exception
  11. Делегирующие конструкторы: https://en.cppreference.com/w/cpp/language/initializer_list.html#Delegating_constructor
  12. Обсуждение new и освобождения памяти при брошенном в конструкторе исключении: https://ru.stackoverflow.com/questions/6137/%D0%91%D1%83%D0%B4%D0%B5%D1%82-%D0%BB%D0%B8-%D1%83%D1%82%D0%B5%D1%87%D0%BA%D0%B0-%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8-%D0%B5%D1%81%D0%BB%D0%B8-%D0%BA%D0%BE%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D0%BE%D1%80-%D0%B1%D1%80%D0%BE%D1%81%D0%B8%D1%82-%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *