Как стать автором
Обновить
0

C++ без new и delete

Время на прочтение15 мин
Количество просмотров88K
Привет, хабравчане!

Меня зовут Михаил Матросов, я технический менеджер в компании Align Technology. Сегодня я поработаю капитаном и немного расскажу об основах современного С++.

Работая над большим проектом, мне часто приходится смотреть чужой код и порой я вижу странное. А именно, многие даже вполне матёрые программисты на С++ могут не знать некоторых фундаментальных для языка вещей. Ну, это даже не слишком удивительно — язык такой.

Мне бы хотелось поговорить об этих основах и начну я со своей любимой темы. Будем говорить об операторах new и delete. А точнее, об их отсутствии. Я расскажу, как писать надёжный и современный код на С++ без использования операторов new и delete.

Казалось бы, тема стара как мир, Саттер и Майерс в своё время всё разложили по полочкам. Именно поэтому я не буду вдаваться в ненужные подробности, отправляя читателей к первоисточникам. Моя цель собрать информацию по вопросу в одном месте, дать соответствующие ссылки и сформулировать ёмкие рекомендации.

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


Изображение взято с сайта behappy.me

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

Мы поступим следующим образом:
  • Сделаем небольшое теоретическое введение.
  • Рассмотрим несколько сценариев и покажем, почему
    • cледует избегать оператора delete;
    • следует избегать оператора new на примере make-функций;
    • следует избегать оператора new на других примерах.
  • Подытожим выводы.

Таким образом мы постепенно придём к пониманию, почему нужно отказаться от использования new и delete в клиентском коде и похоронить их в недрах STL и boost.

Зачем вообще нужны new и delete?


Операции new и delete в С++ нужны для создания и удаления динамических объектов.

Основная особенность динамических объектов в том, что временем их жизни нужно управлять вручную. Противоположность им с этой точки зрения составляют автоматические объекты, временем жизни которых управляет компилятор. Существуют ещё статические и thread-local объекты, но они нам в рамках данной статьи не интересны. См. storage duration.

Автоматические объекты удаляются неявно в соответствии с чёткими правилами, которые реализованы в компиляторе. Локальные переменные функции удаляются, когда поток управления покидает область видимости, в которой они объявлены. Члены класса удаляются после выполнения деструктора этого класса.

А вот для динамических объектов таких правил нет. Их нужно всегда удалять явно (явное удаление может быть скрыто в недрах утилитарных классов и функций). Вот небольшая иллюстрация для лучшего понимания:
struct A
{
  std::string str;  // Автоматический объект, неявно удаляется в деструкторе A (который сгенерирован 
                    // автоматически). Сам строковый буфер - динамический объект (*), будет явно 
                    // удалён в деструкторе std::string, который будет неявно вызван в деструкторе A. 
  // (*) Если только строка не слишком короткая, тогда сработает Small String Optimization и динамический
  // буфер вообще не будет выделен.
};

void foo()
{
  std::vector<int> v;  // Автоматический объект, неявно удаляется при выходе из функции.
  v.push_back(10);  // Содержимое вектора - динамический объект (массив), будет явно удалён в деструкторе
                    // вектора, который будет неявно вызван при выходе из функции.

  A a;  // Автоматический объект класса А, неявно удаляется при выходе из функции.

  A* pa = new A;  // Указатель pa - автоматический объект, неявно удаляется при выходе из функции,
                  // но он указывает на динамический объект класса А, который нужно удалить в явном виде.
  delete pa;  // Явное удаление динамического объекта.

  auto upa =  // Умный указатель upa - автоматический объект, неявно удаляется при выходе из функции, 
    std::make_unique<A>();  // но он указывает на динамический объект класса А, который будет явно удалён
                            // в деструкторе умного указателя.
}

Обычно динамические объекты находятся в куче, хотя в общем случае это не так. Автоматические объекты могут находиться как на стеке, так и в куче. В примере выше автоматический объект upa->str находится в куче, т.к. он — часть динамического объекта *upa. Т.е. свойства динамический/автоматический определяют время жизни, но не место жизни объекта.

Свойство динамический/автоматический принадлежит именно объекту, а не типу, т.к. объекты одного и того же типа могут быть как динамическими, так и автоматическими*). В примере выше объекты a и *pa оба имеют тип А, но первый является автоматическим, а второй — динамическим.

Динамические объекты в С++ создаются с помощью new, а удаляются с помощью delete. Вот отсюда и все проблемы: никто не говорил, что эти конструкции следует использовать напрямую! Это низкоуровневые вызовы, они как бы под капотом. И не нужно лезть под капот без необходимости.

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


* Существуют техники, чтобы ограничить свойство динамический/автоматический на уровне типа. Например, закрытые конструкторы.

В чём проблема с new и delete?


С самого момента своего изобретения операторы new и delete используются неоправданно часто. Самые большие проблемы относятся к оператору delete:
  • Можно вообще забыть вызвать delete (утечка памяти, memory leak).
  • Можно забыть вызвать delete в случае исключения или досрочного возврата из функции (тоже утечка памяти).
  • Можно вызвать delete дважды (двойное удаление, double delete).
  • Можно вызвать не ту форму оператора: delete вместо delete[] или наоборот (неопределённое поведение, undefined behavior).
  • Можно использовать объект после вызова delete (dangling pointer).

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

Поэтому люди давно сообразили прятать оператор delete в недрах контейнеров и умных указателей, убрав тем самым его из клиентского кода. Однако с оператором new тоже связаны проблемы, но для них решения появились не сразу, и, по факту, многие разработчики до сих пор стесняются этими решениями пользоваться. Об этом мы подробнее поговорим, когда дойдём до make-функций.

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

Начнём с простого — с динамических массивов.

Динамические массивы


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

Для выделения динамических массивов С++ на низком уровне предоставляет векторную форму операторов new и delete: new[] и delete[]. В качестве примера рассмотрим некоторую функцию, которая работает с внешним буфером:
void DoWork(int* buffer, size_t bufSize);

Подобные функции часто встречаются в библиотеках с API на чистом С*. Ниже приведён пример, как может выглядеть использующий её код. Это плохой код, т.к. он в явном виде использует delete, а связанные с ним проблемы мы уже описали выше.
void Call(size_t n)
{
  int* p = new int[n];
  DoWork(p, n);
  delete[] p;  // Плохо!
}

Тут всё просто и большинству известно, что для подобных целей в С++ следует использовать стандартный контейнер std::vector**. Он сам выделит память в конструкторе и освободит её в деструкторе. К тому же, он ещё может менять свой размер во время жизни, но для нас это сейчас значения не имеет. С использованием вектора код будет выглядеть так:
void Call(size_t n)
{
  std::vector<int> v(n);  // Лучше.
  DoWork(v.data(), v.size());
}

Тем самым мы решаем все проблемы, связанные с вызовом delete, и к тому же вместо безликой пары указатель+число, имеем явный контейнер с удобным интерфейсом.

При этом никаких new и delete. Не буду более подробно останавливаться на этом сценарии. По моему опыту большинство разработчиков и так знает, что следует делать в данном случае и почему.


* На С++ подобный интерфейс следовало бы реализовать с использованием типа span<int>. Он предоставляет унифицированный STL-совместимый интерфейс для доступа к непрерывным последовательностям элементов, при этом никак не влияя на их время жизни (невладеющая семантика).

** Поскольку эту статью читают программисты на С++, я почти уверен, что кто-то подумает: «Ха! std::vector хранит в себе целых три (!) указателя, когда старый добрый int* — это по определению всего один указатель. Налицо перерасход памяти и нескольких машинных инструкций на их инициализацию! Это неприемлемо!». Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank. Если для вас это действительно проблема, то могу порекомендовать std::unique_ptr<int[]>, а в будущем стандарт может подарить нам dynarray.

Динамические объекты


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

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

К типам со стандартной моделью управления памятью относятся все стандартные типы**, включая контейнеры. В самом деле, контейнер управляет памятью, которую он выделил сам. Ему нет никакого дела до того, кто его создал и как он будет удалён.

К типам с нестандартной моделью управления памятью можно отнести, например, объекты Qt. Здесь у каждого объекта есть родитель, который ответственен за его удаление. И объект об этом знает, т.к. он наследуется от класса QObject. Сюда же относятся типы со счётчиком ссылок, например, рассчитанные на работу с boost::intrusive_ptr.

Иными словами, тип со стандартной моделью управления памятью не предоставляет никаких дополнительных механизмов для управления своим временем жизни. Этим целиком и полностью должна заниматься пользовательская сторона. А вот тип с нестандартной моделью такие механизмы предоставляет. Например, QObject имеет методы setParent() и children() и содержит в себе список детей, а тип boost::intrusive_ptr опирается на функции intrusive_ptr_add_ref и intrusive_ptr_release и содержит в себе счётчик ссылок.

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

Далее рассмотрим объекты обеих моделей. Забегая вперёд, стоит сказать, что для объектов со стандартным управлением памятью однозначно не стоит использовать new и delete в клиентском коде, а для объектов с нестандартным — зависит от конкретной модели.


* Некоторые исключения: идиома pimpl; очень большой объект (например, буфер памяти).

** Исключение составляет std::locale::facet (см. дальше).

Динамические объекты со стандартным управлением памятью


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

Собственно, умные указатели, да, это ответ. Именно им следует отдать управление временем жизни динамических объектов. Их в С++ целых два: std::shared_ptr и std::unique_ptr. Не будем здесь выделять std::weak_ptr, т.к. это просто помощник для std::shared_ptr в определённых сценариях использования.

Что касается std::auto_ptr, он был официально исключён из С++ начиная с С++17. Покойся с миром!

Не буду здесь останавливаться на устройстве и использовании умных указателей, т.к. это выходит за рамки статьи. Сразу напомню, что они идут в комплекте с замечательными функциями std::make_shared и std::make_unique, и именно их следует использовать для создания умных указателей.

Т.е. вместо вот такого:
std::unique_ptr<Cookie> cookie(new Cookie(dough, sugar, cinnamon));

следует писать вот так:
auto cookie = std::make_unique<Cookie>(dough, sugar, cinnamon);

Преимущества make-функций над явным созданием умных указателей прекрасно описаны Гербом Саттером в его GotW #89 и Скоттом Майерсом в его Effective Modern C++, Item 21. Не буду повторяться, лишь приведу здесь краткий список тезисов:
  • Для обеих make-функций:
    • Безопасность с точки зрения исключений.
    • Нет дублирования имени типа.
  • Для std::make_shared:
    • Выигрыш в производительности, т.к. контрольный блок выделяется рядом с самим объектом, что уменьшает количество обращений к менеджеру памяти и увеличивает локальность данных. Оптимизация We Know Where You Live.

У make-функций имеется и ряд ограничений, подробно описанных в тех же источниках:
  • Для обеих make-функций:
    • Нельзя передать свой deleter. Это вполне логично, т.к. внутри себя make-функции по определению используют стандартный new.
    • Нельзя использовать braced initializer, а также все прочие тонкости, связанные с perfect forwarding (см. Effective Modern C++, Item 30).
  • Для std::make_shared:
    • Потенциальный перерасход памяти для больших объектов при долгоживущих слабых ссылках (std::weak_pointer).
    • Проблемы с операторами new и delete переопределёнными на уровне класса.
    • Потенциальное ложное разделение (false sharing) между объектом и контрольным блоком (см. вопрос на StackOverflow).

На практике указанные ограничения встречаются редко и не умаляют преимуществ. Получается, что умные указатели скрыли от нас вызов delete, а make-функции скрыли от нас вызов new. В итоге мы получили более надёжный код, в котором нет ни new, ни delete.

Кстати, устройство make-функций серьёзно раскрывает в своих докладах Стефан Лававей (a.k.a. STL). Приведу здесь красноречивый слайд из его доклада Don’t Help the Compiler:

Динамические объекты с нестандартным управлением памятью


Помимо стандартного подхода управления памятью через умные указатели встречаются и другие модели. Например, подсчёт количества ссылок (reference counting) и отношения родитель-ребёнок (parent to child relationship).

Далее рассмотрим несколько примеров с разной моделью памяти и попробуем сделать нашу жизнь легче за счёт избавления от new и delete. Где-то у нас это получится, а где-то нет.

Динамические объекты с подсчётом ссылок



Очень часто встречающийся приём, используемый во многих библиотеках. Рассмотрим в качестве примера библиотеку OpenSceneGraph. Это открытый кроссплатформенный 3D-движок, написанный на С++ и OpenGL.

Большая часть классов в нём наследуется от класса osg::Referenced, который осуществляет внутри себя подсчёт ссылок. Метод ref() увеличивает счётчик, метод unref() уменьшает счётчик и удаляет объект, когда счётчик опускается до нуля.

В комплекте также идёт умный указатель osg::ref_ptr<T>, который вызывает метод T::ref() для хранимого объекта в своём конструкторе и метод T::unref() в деструкторе. Такой же подход используется в boost::intrusive_ptr, только там вместо методов ref() и unref() выступают внешние функции.

Рассмотрим фрагмент кода, который приведён в официальном руководстве OpenSceneGraph 3.0: Beginner's guide:
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
// ... 
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
// ... 
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ... 

Очень знакомые конструкции вида osg::ref_ptr<T> p = new T. Абсолютно аналогично тому, как функции std::make_unique и std::make_shared служат для создания классов std::unique_ptr и std::shared_ptr, мы можем написать функцию osg::make_ref для создания класса osg::ref_ptr. Делается это очень просто, по аналогии с функцией std::make_unique:
namespace osg
{
  template<typename T, typename... Args>
  osg::ref_ptr<T> make_ref(Args&&... args)
  {
    return new T(std::forward<Args>(args)...);
  }
}

Перепишем этот фрагмент кода вооружившись нашей новой функцией:
auto vertices = osg::make_ref<osg::Vec3Array>();  
// ... 
auto normals = osg::make_ref<osg::Vec3Array>();  
// ... 
auto geom = osg::make_ref<osg::Geometry>();
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ... 

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

Вызов delete уже был спрятан в методе osg::Referenced::unref(), а теперь мы спрятали и вызов new в функции osg::make_ref. Так что никаких new и delete.


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

Динамические объекты для немодальных диалогов в MFC



Рассмотрим пример, специфичный для библиотеки MFC. Это обёртка из классов С++ над Windows API. Она используется для упрощения разработки GUI под Windows.

Интересен приём, которым Microsoft официально рекомендует пользоваться для создания немодальных диалогов. Т.к. диалог немодальный, не совсем ясно, кто ответственен за его удаление. Предлагается ему удалять себя самому в переопределённом методе CDialog::PostNcDestroy(). Этот метод вызывается после обработки сообщения WM_NCDESTROY — последнего сообщения, получаемого окном в его жизненном цикле.

В примере ниже диалог создаётся по нажатию на кнопку в методе CMainFrame::OnBnClickedCreate() и удаляется в переопределённом методе CMyDialog::PostNcDestroy().
void CMainFrame::OnBnClickedCreate()
{
  auto* pDialog = new CMyDialog(this);
  pDialog->ShowWindow(SW_SHOW);
}

class CMyDialog : public CDialog
{
public:
  CMyDialog(CWnd* pParent)
  {
    Create(IDD_MY_DIALOG, pParent);
  }

protected:
  void PostNcDestroy() override
  {
    CDialog::PostNcDestroy();
    delete this;
  }
};

Здесь у нас не спрятан ни вызов new, ни вызов delete. Способов выстрелить себе в ногу — масса. Помимо обычных проблем с указателями, можно забыть переопределить в своём диалоге метод PostNcDestroy(), получим утечку памяти. При виде вызова new, может возникнуть желание самостоятельно вызвать в определённый момент delete, получим двойное удаление. Можно случайно создать объект диалога в автоматической памяти, снова получим двойное удаление.

Попробуем спрятать вызовы к new и delete внутри промежуточного класса CModelessDialog и фабрики CreateModelessDialog, которые будут отвечать в нашем приложении за немодальные диалоги:
class CModelessDialog : public CDialog
{
public:
  CModelessDialog(UINT nIDTemplate, CWnd* pParent)
  {
    Create(nIDTemplate, pParent);
  }

protected:
  void PostNcDestroy() override
  {
    CDialog::PostNcDestroy();
    delete this;
  }
};

// Фабрика для создания модальных диалогов
template<class Derived, typename... Args>
Derived* CreateModelessDialog(Args&&... args)
{
  // Вместо static_assert в теле функции, можно использовать std::enable_if в её заголовке, что позволит нам использовать SFINAE. 
  // Но т.к. вряд ли ожидаются другие перегрузки этой функции, разумным выглядит использовать более простое и наглядное решение.
  static_assert(std::is_base_of<CModelessDialog, Derived>::value, 
                "CreateModelessDialog should be called for descendants of CModelessDialog");
  auto* pDialog = new Derived(std::forward<Args>(args)...);
  pDialog->ShowWindow(SW_SHOW);
  return pDialog;
}

Класс сам переопределяет метод PostNcDestroy(), в котором мы спрятали delete, а для создания классов наследников используется фабрика, в которой мы спрятали new. Создание и определение класса наследника теперь выглядит так:
void CMainFrame::OnBnClickedCreate()
{
  CreateModelessDialog<CMyDialog>(this);
}

class CMyDialog : public CModelessDialog
{
public:
  CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) {}
};

Конечно, подобным образом мы не решили всех проблем. Например, объект всё равно можно выделить на стеке и получить двойное удаление. Запретить выделение объекта на стеке можно только путём модификации самого класса объекта, например добавлением закрытого конструктора. Но мы никак не можем этого сделать из базового класса CModelessDialog. Можно, конечно, вообще сокрыть класс CMyDialog и сделать фабрику не шаблонной, а более классической, принимающей некоторый идентификатор класса. Но это всё уже выходит за рамки статьи.

Так или иначе, мы упростили создание диалога из клиентского кода и написание нового класса диалога. И при этом мы убрали из клиентского кода вызовы new и delete.

Динамические объекты с отношением родитель-ребёнок



Встречаются достаточно часто, особенно в библиотеках для разработки GUI. В качестве примера рассмотрим Qt — хорошо известную библиотеку для разработки приложений и UI.

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

Отличный пример ситуации, когда избавиться от new и delete так просто не получится. Библиотека проектировалась таким образом, что эти операторы можно и нужно применять во многих случаях. Я предлагал обёртку для создания объектов с ненулевым родителем, но идея не пошла (см. обсуждение в Qt mailing list).

Таким образом, мне неизвестен хороший способ избавиться от new и delete в Qt.

Динамические объекты std::locale::facet



Для управления выводом данных в потоки в С++ используются объекты std::locale. Локаль является набором фасетов (facet), которые определяют способ вывода тех или иных данных. Фасеты имеют свой счётчик ссылок и при копировании локалей не происходит копирования фасетов, копируется лишь указатель и увеличивается счётчик ссылок.

Локаль сама ответственна за удаление фасетов, когда счётчик ссылок падает до нуля, но вот создавать фасеты должен пользователь, используя оператор new (см. секцию Notes в описании конструктора std::locale):
std::locale default;
std::locale myLocale(default, new std::codecvt_utf8<wchar_t>);

Этот механизм был реализован ещё до внедрения стандартных умных указателей и выбивается из общих правил применения классов стандартной библиотеки.

Можно сделать простую обёртку, создающую локаль, чтобы убрать new из клиентского кода. Однако это достаточно известное исключение из общих правил, и может быть, нет смысла городить ради него огород.

Заключение


Итак, сначала мы рассмотрели такие сценарии, как создание динамических массивов и динамических объектов со стандартным управлением памятью. Вместо new и delete мы использовали стандартные контейнеры и make-функции и получили более простой и надёжный код.

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

Тем не менее, в большинстве случаев эта рекомендация даёт отличные результаты, и можно использовать её в качестве принципа по умолчанию. Теперь мы можем считать, что, если код использует new или delete, это особый случай, который требует особого внимания. Если вы видите эти вызовы в клиентском коде, задумайтесь, действительно ли они оправданы.

Список рекомендаций:
  • Избегайте использования new и delete в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью.
  • Используйте стандартные контейнеры для динамических структур данных.
  • Используйте make-функции для создания динамических объектов, когда это возможно.
  • Создавайте обёртки для объектов с нестандартной моделью памяти.

От автора


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

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

Чуть больше года назад я выступал с докладом на эту тему на конференции C++ Russia. После моего выступления аудитория разделилась на две группы: те, для кого всё было очевидным, и те, кто сделал для себя замечательное открытие. Полагаю, что на конференции чаще ходят уже достаточно опытные разработчики, так что, если даже среди них было множество людей, для кого эта информация была в новинку, я надеюсь, что эта статья будет полезна для сообщества.

PS В процессе обсуждения статьи, у нас с коллегами разгорелся целый спор, как правильно: «Майерс» или «Мейерс». С одной стороны, для русского слуха более привычно звучит «Мейерс», и мы сами вроде бы всегда говорили именно так. С другой стороны, на вики используется именно «Майерс». Если посмотреть локализованные книги, то там вообще кто во что горазд: к этим двум вариантам прибавляется ещё и «Мэйерс». На конференциях разные люди представляют его по-разному. В конечном итоге нам удалось выяснить, что сам себя он называет именно «Майерс», на чём и порешили.

Ссылки


  1. Herb Sutter, GotW #89 Solution: Smart Pointers.
  2. Scott Meyers, Effective Modern C++, Item 21, p. 139.
  3. Stephan T. Lavavej, Don’t Help the Compiler.
  4. Bjarne Stroustrup, The C++ Programming Language, 11.2.1, p. 281.
  5. Five Popular Myths about C++., Part 2
  6. Mikhail Matrosov, C++ without new and delete.
Теги:
Хабы:
+59
Комментарии134

Публикации

Изменить настройки темы

Информация

Сайт
www.aligntech.com
Дата регистрации
Численность
1 001–5 000 человек
Местоположение
США

Истории