Безопасные конструкторы

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

    A::A(int x) : b(x) {}

    Или хранить их по ссылке:

    A::A(int x) { b = new B(x); }

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

    Начнём по-порядку. Пусть у нас есть некий класс, конструктор которого, в некоторых случаях, может вызывать исключение (нет файла, нет связи, не подошёл пароль, недостаточно прав для выполнения операции… что угодно). Наш класс будет предельно прост и предсказуем.

    class X {
    private:
      int xx;
    public:
      X(int x) {
        cout << "X::X x=" << x << endl;
        if (x == 0) throw(exception());
        xx = x;
      }
      ~X() {
        cout << "X::~X x=" << xx << endl;
      }
    };

    То есть, если аргумент конструктора равен нулю, то запускается исключение.

    Допустим, нам нужен некий класс, объекты которого должны содержать по два объекта класса X.

    Вариант первый — с указателями (осторожно, опасный код!)


    Делаем всё по-науке:

    class Cnt {
    private:
      X *xa;
      X *xb;
    public:
      Cnt(int a, int b) {
        cout << "Cnt::Cnt" << endl;
        xa = new X(a);
        xb = new X(b);
      }
      ~Cnt() {
        cout << "Cnt::~Cnt" << endl;
        delete xa;
        delete xb;
      }
    };

    Казалось бы, ничего не забыли. (Хотя, строго говоря, конечно забыли как минимум конструктор копирования и операцию присвоения, которые бы корректно работали с нашими указателями; ну да ладно.)

    Воспользуемся этим классом:

    try {
      Cnt c(1, 0);
    } catch (...) {
      cout << "error" << endl;
    }

    И разберёмся, что и когда будет конструиваться и уничтожаться.

    • Сперва запустится процесс создания объекта Cnt.
    • В нём будет создан объект *xa
    • Начнёт создание объекта *xb...
    • … и тут произойдёт исключение

    Всё. Конструктор прекратит свою работу, а деструктор объекта Cnt вызван не будет (и это правильно, объект-то не создался). Итого, что мы имеем? Один объект X, указатель на который (xa) навсегда потерян. В этом месте мы сразу получаем утечку памяти, а возможно, получаем утечку и более ценных ресурсов, соктов, курсоров...

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

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

    Какие же есть решения?

    Самое простое, надёжное и естественное решение — хранить объект по значению


    Пример:

    class Cnt {
    private:
      X xa;
      X xb;
    public:
      Cnt(int a, int b) : xa(a), xb(b) {
        cout << "Cnt::Cnt" << endl;
      }
      ~Cnt() {
        cout << "Cnt::~Cnt" << endl;
      }
    };

    Это компактно, это элегантно, это естественно… но главное — это безопасно! В этом случае компилятор следит за всем происходящим, и (по-возможности) вычищает всё, что уже не понадобится.

    Результат работы кода:

    try {
      Cnt c(1, 0);
    } catch (...) {
      cout << "error" << endl;
    }

    будет таким:

    X::X x=1
    X::X x=0
    X::~X x=1
    error

    То есть объект Cnt::xa был автоматически корректно уничтожен.

    Безумное решение с указателями


    Настоящим кошмаром может стать вот такое решение:

    Cnt(int a, int b) {
      cout << "Cnt::Cnt" << endl;
      xa = new X(a);
      try {
        xb = new X(b);
      } catch (...) {
        delete xa;
        throw;
      }
    }

    Представляете, что будет, если появится Cnt::xc? А если придётся поменять порядок инициализации?.. Надо будет приложить не мало усилий, чтобы ничего не забыть, сопровождая такой код. И, что самое обидное, это вы сами для себя же разложили везде грабли.

    Лирическое отступление про исключения.


    Для чего были придуманы исключения? Для того, чтобы отделить описание нормального хода программы от описания реакции на какие-то сбои.

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

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

    Это делает код запутанным и трудным для понимания и поддержки.

    Решение настоящих индейцев — умные указатели


    Если вам всё же необходимо хранить указатели, то вы всё равно можете обезопасить свой код, если сделаете для указателей обёртки. Их можно писать самим, а можно использовать множество, уже существующих. Пример использования auto_ptr:

    class Cnt {
    private:
      auto_ptr<X> ia;
      auto_ptr<X> ib;
    public:
      Cnt(int a, int b) : ia(new X(a)), ib(new X(b)) {
        cout << "Cnt::Cnt" << endl;
      }
      ~Cnt() {
        cout << "Cnt::~Cnt" << endl;
      }
    };

    Мы практически вернулись к решению с хранением членов класса по значению. Здесь мы храним по значению объекты класса auto_ptr<X>, о своевременном удалении этих объектов опять заботится компилятор (обратите внимание, теперь нам не надо самостоятельно вызывать delete в деструкторе); а они, в свою очередь, хранят наши указатели на объекты X и заботятся о том, чтобы память вовремя освобождалась.

    Да! И не забудьте подключить

    #include <memory>

    Там описан шаблон auto_ptr.

    Лирическое отступление про new


    Одно из преимуществ C++ перед C состоит в том, что C++ позволяет работать со сложными структурами данных (объектами), как с обычными переменными. То есть C++ сам создаёт эти структуры и сам удаляет их. Программист может не задумываться об освобождении ресурсов до тех пор, пока он (программист) не начнёт сам создавать объекты. Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно. И это не только деструкторы. Больше того, вам скорее всего придётся самостоятельно реализовывать операцию копирования и операцию присвоения… Одним словом, вы отказались от услуг С++ и попали на весьма зыбкую почву.

    Конечно, в реальной жизни часто приходится использовать «new». Это может быть связано со спецификой алгоритма, продиктовано требованиями по производительности или просто навязано чужими интерфейсами. Но если у вас есть выбор, то наверно стоит трижды подумать, прежде, чем написать слово «new».

    Всем успехов! И пусть ваша память никогда не течёт!

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 58

      –1
      Начнём по-прядку -> Начнём по-порядку
      опечатались
        0
        Спасибо! Исправил.
          0
          Отличная статья для ввода наCИльников и функциональных программистов в ООП C++ :)
        0
        Тяжело обойтись при работе с динамическими данными без динамической памяти.
          0
          Use vector Luk.
            +1
            RAII этому не мешает.
            В статье эта техника и описана, но только с частным случаем — умными указателями. Точно так же можно работать с любыми ресурсами, которые надо гарантированно освободить.
            0
            Браво автор.
            • НЛО прилетело и опубликовало эту надпись здесь
                +1
                В тексте, который вы заменили "..." (там целый абзац) я говорю о том, что C++ сам управляет ресурсами. А если вы используете new/delete, то вы берёте управление в свои руки и отказываетесь от услуг С++. Я не делаю акцента на словах new и delete; акцент на слове «управлять».
                • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  new в контексте безопасности выделения памяти в первом приближении — это всего лишь гибрид malloc и sizeof.

                  Кроме того, new/delete позволяют переопределить алгоритм выделения памяти «на лету», тогда как malloc гвоздями зашит в стандартную библиотеку C.

                  Понятно, что malloc/free в конструкторах/деструкторах использовать в общем случае ещё хуже, чем new/delete.
                  • НЛО прилетело и опубликовало эту надпись здесь
                  +2
                  Еще напишите, почему нельзя так:
                  class Cnt {
                  private:
                    X* ia;
                    X* ib;
                  public:
                    Cnt(int a, int b) : ia(new X(a)), ib(new X(b)) {
                  
                    +1
                    Интересная идея, мне как-то в голову не приходило :-)
                    Может быть оставить этот вопрос читателям для самостоятельно изучения ,-)
                    Подсказка:
                    чем отличается
                    Cnt(int a, int b): ia(new X(a)), ib(new X(b)) {}
                    и
                    Cnt(int a, int b) { ia = new X(a); ib = new X(b); }
                    ?
                      0
                      > чем отличается…
                      Практически ничем. Утечка памяти происходит и там и там.
                        0
                        Я имею ввиду, почему так нельзя:

                        class Cnt {
                        private:
                        X* ia;
                        X* ib;
                        public:
                        Cnt(int a, int b): ia(new X(a)), ib(new X(b)) {
                        }
                        ~Cnt() {
                        delete a; delete b;
                        }
                        }
                        0
                        Потому что в этом случае также не вызывается деструктор для объекта, на который указывает ia, если при вызове «new X(b)» выбрасывается исключение.
                          0
                          И ещё почему нельзя так:
                          foo(auto_ptr<T>(new T), auto_ptr<T>(new T));
                            0
                            По-идее по завершение функции эти указатели, как временные объекты, будут удалены, а следовательно в их деструкторах будут удалены данные. Наверное, если вы не будете в дальнейшем использовать эти объекты, то так вызывать можно.
                              0
                              Порядок вычисления аргументов в функциях неопределён, поэтому в принципе возможна ситуация, когда оба new выполнятся до конструкции соответствующих им auto_ptr и при бросании исключения вторым из них, память после первого уже не освободится.
                              Случай описан у Саттера, но не помню в какой именно книге.
                                0
                                Еще хорошо описана безопасность конструкторов в книжке Брюса Эккеля «Философия C++. Том 2: Практическое программирование.»
                                  0
                                  у Мэйерса это описано, в эффективном использовании с++
                            +1
                            Это все хорошо, что индейцы пользуются auto_ptr

                            но что произойдет при присваивании?
                            в вашем варианте нет приватных перегруженных конструктора копирования(КК) и оператора = (ОП)

                            после вызова

                            Cnt a(1,2);
                            Cnt b(3,4);

                            a=b;

                            объект b передаст владение памятью, содержащей 3 и 4 объекту a

                            вы бы поостереглись советы такие давать.

                            Если уж индейцы такие настоящие то у них несколько вариантов

                            1) запретить копирование объекта закрыв КК и ОП
                            2) перегрузить КК и ОП и предотвратить слепое копирование auto_ptr
                            3) использовать boost::shared_ptr

                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                о том и речь

                                этот абзац относится к параграфу
                                «Вариант первый — с указателями (осторожно, опасный код!)»

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

                                  +1
                                  Коллеги! Спасибо за комментарии. Копирование и присвоение — очень важно. Я напишу про это, когда будет время. Но эта заметка немного про другое.
                                    0
                                    Пишите ещё :)
                              • НЛО прилетело и опубликовало эту надпись здесь
                                –1
                                >Это делает код запутанным и трудным для понимания и поддержки.
                                А мне казалось что это делает код понятным, так как не надо держать в голове все хитрости крестов.
                                Может у меня память плохая, но когда делаю что-то на крестах, то не в состоянии в голове удерживать все эти мелочи, которые описаны в куче in-depth книг. А как у вас с этим?
                                  +2
                                  Мне кажется, мы говорим о разных вещах. Что вы понимаете под словом «Это»? (Я — разделение нормального хода программы и обработки ошибок; разве это разделение делает код запутанным?) Что вы понимаете под «хитрости крестов»? Мне не кажется, что конструкторы, указатели или исключения являются какими-то «хитростями»… в этой заметке нет никаких хитростей (вот в прошлой моей заметке были хитрости, а тут — нет).
                                    +1
                                    >(Я — разделение нормального хода программы и обработки ошибок; разве это разделение делает код запутанным?)
                                    На Си обычно тоже разделяют ход програмы и обработку ошибок. Тут уже больше зависит от того кто пишет код.

                                    >Мне не кажется, что конструкторы, указатели или исключения являются какими-то «хитростями»…
                                    Ну конструкторы и указатели не являются хитростями, а вот писать «exception-safe» код на Си++ — это уже для меня шаманство, из-за которого мои програмы текут, падают итд :) Я понимаю, что это недостаток моих знаний, но сколько ++ников реально умеет писать «exception-safe» код?
                                      0
                                      > На Си обычно тоже разделяют ход програмы и обработку ошибок. Тут уже больше зависит от того кто пишет код.

                                      Погодите. Вот у вас есть кусок кода, в котором есть несколько wirte. В C вы должны непосредственно(!) после каждого проверить его результат, и если там -1, то хорошо бы проверить errno. В С++ вы можете заключить всё это дело в один большой try и отделить обработку исключений.

                                      Больше того, вы можете написать библиотеку с вашими write, а обработку ошибок (исключений) оставить на усмотрение пользователя вашей библиотеки. А он пускай решает, валиться ли ему, или пытаться восстановиться… Это гуманно по отношению к пользователю :-)

                                      >… писать exception-safe код на Си++…

                                      Тут спорить с вами не буду. Надо ли восстанавливаться после исключений — это большой и неоднозначный вопрос. Но иногда очень хочется восстановиться :-) И это возможно!
                                        +2
                                        >В С++ вы можете заключить всё это дело в один большой try и отделить обработку исключений
                                        Втыкаем label exception: отвечающий за обработку ошибок и после каждого write делаем if..goto прямиком туда (некоторые заварачивают всё в красивые макросы). Скорее всего код получится тормознее из-за кучи if'ов, если используются zcx exceptions(вроде так их обозвали в gcc — плохо разбираюсь в этой теме).
                                        И с райтом плохой пример, так как он возвращает всякие EAGAIN(или вы их тоже эксепшенами ловите?)… но это редкое исключение из правил :)

                                        >а обработку ошибок (исключений) оставить на усмотрение пользователя вашей библиотеки.
                                        В наших кодинг гайдлайнс уже много лет нет пунктика — игнорировать ошибки ;)

                                        >Надо ли восстанавливаться после исключений — это большой и неоднозначный вопрос.
                                        хм… всегда казалось, что в большинстве случаев надо восстанавливаться.
                                        Но я про другой «exception-safe»

                                        Ну и может кто-то ещё не видел FQA :)
                                        C++ FQA >> exceptions
                                          –1
                                          >Скорее всего код получится тормознее из-за кучи if'ов, если используются zcx exceptions(вроде так их обозвали в gcc — плохо разбираюсь в этой теме).
                                          Плохо сформулировал :) Код на Си с ifами получится тормознее, чем на крестах с zero-cost исключениями.
                                            0
                                            1.1) goto — зло. аргументировать?
                                            1.2) макросы — зло :-) аргументировать?
                                            1.3) вот на счёт тормознутости — очень не уверен. Исключения — не бесплатная вещь. Для их обработки в стеке создаются специальные конструкции… Сейчас компиляторы очень далеко продвинулись и многое создаётся во время компиляции, но не всё. Думаю ответ на вопрос «что быстрее» будет зависеть и от компилятора и от характера кода (кол-во вложенных блоков, кол-во переменных и прочего); и if-ы вполне могут рассчитывать на победу в этой гонке :-)
                                            1.4) я не считаю EAGAIN исключениями… как и write я не считаю ОО-путём :-) Я привёл пример из С.

                                            2.1) Не игнорировать ошибки — это хорошо. Вопрос, что и когда(!) с ними делать. Исключения позволяют ответить на «когда» и «внутри библиотеки» и «вне библиотеки». Это очень удобно.

                                            3.1) На сколько я понимаю. В С++ исключения разрабатывались… универсальными, но всё же больше невосстанавливающимися, чем восстанавливающимися. Достаточно вспомнить про проблемы с исключениями в деструкторах.
                                              0
                                              >1.1) goto — зло. аргументировать?
                                              >1.2) макросы — зло :-) аргументировать?
                                              Не надо :) Этими аргументами из книжек по плюсам воспитывают крестовых новобранцев, но мы то с вами уже понимаем в каких ситуациях их использование оправдано ;) Мы не живём в идеальном миру о котором мечтают идеологи плюсов, нам приходиться решать реальные проблемы с тормозными, багнутыми цпп компиляторами итд.

                                              >вот на счёт тормознутости — очень не уверен.
                                              я про zero-cost (zcx) исключения, которые с помощью libunwind'а реализованы(про виндовые средства разработки ничего не знаю). Там большой оверхед только когда происходит исключение, зато в обычной ситуации всё шустренько.

                                              >Вопрос, что и когда(!) с ними делать.
                                              Если пишите «exception-safe» код, то да… Но моя практика общения с типичными кодерами, вроде меня, показала что писать «exception-safe» код на ++ многие не умеют.
                                              А иначе код ничем не будет отличаться от варианта на Си.

                                              >3.1) На сколько я понимаю. В С++ исключения разрабатывались… универсальными, но всё же больше невосстанавливающимися, чем восстанавливающимися. Достаточно вспомнить про проблемы с исключениями в деструкторах.
                                              Проблем у них много, но это не значит что от исключений приложение должно умирать (или мы про какое-то другое восстановление?)
                                                0
                                                Нуу… Вы меня убедили! Спасибо!
                                                Жизнь действительно не такая, как хотелось бы; тут спорить не с чем. Но я надеюсь чуть-чуть бриблизить её к идеалу :-) Поэтому и написал это :-)
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                      +1
                                      «Пусть у нас есть некий класс, конструктор которого, в некоторых случаях, может вызывать исключение»
                                      На мой взгляд, лучше не писать конструкторов, которые могут приводить к исключениям. Я бы предложил использовать связку конструктор + дополнительный инициализирующий метод (например, setup()).
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                          0
                                          Для этого примера ответ классический — ничего. Все опасные действия (грозящие исключением) будут в setup().
                                            0
                                            Дело в том, что, на мой взгляд, при создании «опасных» конструкторов (с возможностью возникновения исключений в них), программист идет против «философии» языка. Конструктор в C++ должен создавать объект, имеющий некое стабильное состояние (даже если он в этом состоянии непригоден для использования). Все исключения — после, когда объект уже создан, в инициализирующем методе.
                                            Исключения в конструкторах — прерогатива языков со сборщиками мусора.
                                              +2
                                              Ну это вопрос спорный. Во-первых, если назвать «стабильным» непригодное состояние, то что же такое «нестабильное состояние»? Во-вторых… вы считаете, что создавать непригодные для использования объекты это хорошее решение? Мне кажется, что это весьма опасно. При создании такого объекта, каждый раз (каждый!) надо не забыть исполнить определённый ритуальный танец. Мне кажется, это очень коварные объекты.

                                              Кроме того, нельзя полностью избежать исключений в конструкторах. Такова уж природа конструкторов. Объекту для создания может чего-то не хватить. Даже самом недоделанному.
                                                0
                                                Не опасно. Поведение объектов документируется, и ритуальный танец (состоящий из одного вызова) описывается.
                                                Эти объекты ничуть не коварнее прочих. Вы ведь вызываете open перед началом работы с файлом? Если созданный объект непригоден для использования, то это должно тут же обнаруживаться, например, с помощью assertions.

                                                Кроме того, нельзя полностью избежать исключений в конструкторах
                                                Как минимум, пустой конструктор не бросит исключения, если такового не бросает базовый класс.
                                                  0
                                                  > Не опасно.
                                                  Три «если»: 1) документация есть и не устарела, 2) документация была прочитана 3) про танец не забыли. Не много ли?
                                                  Кроме того, если логика программы хоть сколько-то нетривиальна, то можно получить двойную инициализацию, а это бывает ещё хуже.

                                                  > Вы ведь вызываете open перед началом работы с файлом?
                                                  Нет:
                                                  #include #include using namespace std;
                                                  int main() {
                                                  ifstream in(«io.cpp»);
                                                  if (!in) {
                                                  cout << «ERROR» << endl;
                                                  return 1;
                                                  }
                                                  char buf[1024];
                                                  in.get(buf, 1024);
                                                  cout << «First line: » << buf << endl;
                                                  return 0;
                                                  }
                                                  (хотя это скорее контр-пример :-))

                                                  > пустой конструктор не бросит исключения, если такового не бросает базовый класс

                                                  И это снова не единственное «если».

                                                  Код, который работает только если всё написали, всё прочитали, ничего не забыли, никто ничего не бросил… такой код я и считаю небезопасным. статья как раз про то, как такого избежать.
                                                    0
                                                    assert (я_инициализирован)
                                                    assertions позволяют вам иметь столько контроля над проинициализированностью объекта, сколько вам хочется.

                                                    Код, который работает только если всё написали, всё прочитали, ничего не забыли, никто ничего не бросил… такой код я и считаю небезопасным. статья как раз про то, как такого избежать.
                                                    Вообще-то это довольно точное определение работающего кода. Я бы с большим интересом прочитал статью о методах написания кода, всегда работающего правильно, если указанные выше условия не выполнены :)
                                        +2
                                        > Безумное решение с указателями…

                                        По-моему не такое уж и безумное.

                                          Cnt(int a, int b, int c, int d) {
                                        	xa = xb = xc = xd = 0;
                                        	try {
                                        	  xa = new X(a);
                                        	  xb = new X(b);
                                        	  xc = new X( c);
                                        	  xd = new X(d);
                                        	} catch (...) {
                                        	  if (xa) delete xa;
                                        	  if (xb) delete xb;
                                        	  if (xc) delete xc;
                                        	  if (xd) delete xd;
                                        	  throw;
                                        	}
                                          }
                                        

                                        Немного криво выглядит, но экспоненциального роста сложности кода при увеличении числа членов класса Cnt, как видите, не происходит.
                                          +2
                                          Кстати
                                          if (xa) delete xa;

                                          можно заменить на
                                          delete xa;

                                          потому что «delete 0» совершенно валидная констукция, которая просто ничего не удаляет :)
                                            0
                                            Хм… тут не поспоришь :-) Но почему-то это решение не кажется мне самым красивым :-)
                                              0
                                              А такое?

                                              class Clazz {
                                              public:
                                              Clazz() try
                                              {
                                              xa = xb = xc = xd = 0;
                                              xa = new X(a);
                                              xb = new X(b);
                                              xc = new X( c);
                                              xd = new X(d);
                                              }
                                              catch(...)
                                              {
                                              _CleanUp();
                                              }

                                              public:
                                              ~Clazz()
                                              {
                                              _CleanUp();
                                              }

                                              private:
                                              void _CleanUp() { /*тут корректно чистим*/ }
                                              };
                                                0
                                                Постарался причесать.

                                                class Clazz
                                                {
                                                public:
                                                Clazz() try
                                                {
                                                xa = xb = xc = xd = 0;
                                                xa = new X(a);
                                                xb = new X(b);
                                                xc = new X( c);
                                                xd = new X(d);
                                                }
                                                catch(...)
                                                {
                                                _CleanUp();
                                                }

                                                public:
                                                virtual ~Clazz()
                                                {
                                                _CleanUp();
                                                }

                                                private:
                                                NOTHROW void _CleanUp() { /*тут корректно чистим*/ }
                                                };
                                                  0
                                                  Даа… причесали вы не на шутку… стало очень прилично… но всё же лично я не стал бы так делать :-) Хотя, за виртуальный деструктор — отдельный респект! :-)
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                            0
                                            Есть такая парадигма в программировании, как exception-safe constructor. Я ее придерживаюсь и вам рекомендую. Накладные расходы — дополнительный метод Init.
                                              +1
                                              Даже странно, что ещё никто не вспомнил Скотта Мейерса с его 10м советом из книги «Наиболее эффективное использование с++». Та же логика рассуждений, те же std::auto_ptr от которых он в следующей книге отрекается в пользу boost::shared_ptr.

                                              Вообщем как это не прискорбно говорить, но классику жанра всё-таки нужно читать.
                                                0
                                                Под следующей книгой Майерса имеется в виду Effective STL?
                                              0
                                              Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно.
                                              Неправильно. Как только вы написали new, вы обязаны завернуть результат в умный указатель и забыть про delete вообще.

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

                                              Самое читаемое