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

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

Переопределения уровня класса new и delete

В заголовке статьи говорится, что для этого я использовал шаблоны и переопределения new/delete на уровне класса. Давайте на мгновение посмотрим, как C++ выполняет размещение объектов. По своей сути среда выполнения C++ использует некоторые варианты malloc и free для запроса памяти у операционной системы при вызове new и delete соответственно. Поскольку мы можем переопределить эти операторы на уровне класса, мы можем контролировать процесс выделения объекта определенного типа. Зная это, мы можем реализовать решение, которое позволит нам перехватывать выделение памяти и управлять им самостоятельно.

Проблема статических членов

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

Давайте рассмотрим пример.

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

А вот код класса CAllocationProxy:

Приведенный выше класс принимает два параметра шаблона, TPool и TClass.

#ifndef ALLOCATION_PROXY_H
#define ALLOCATION_PROXY_H

#include <new>

template < typename TPool, typename TClass >
class CAllocationProxy
{
public:
 CAllocationProxy()
 {
 };

 virtual
 ~CAllocationProxy()
 {
 };

 static void
 InitProxyInstance()
 {
  // Perform required initialization here
 };

 static 
 void* operator new ( size_t nSize )
 {
  void* p = NULL;
  // perform required allocation calls here  
  return p;
 };

 static 
 void operator delete ( void *p, size_t nSize ) throw()
 {
  if ( !p )
  {
   return; // do nothing on null pointer
  }

  // perform dealocation here
 };

private:
 static 
 TPool m_Pool;
};

template< typename TPool, typename TClass >
TPool CAllocationProxy<TPool,TClass>::m_Pool;
#endif // ALLOCATION_PROXY_H

Остальное говорит само за себя.

Давайте посмотрим, как можно использовать этот код:

(псевдокод)

  • TClass — это тип объектов, которые будут выделены диспетчером памяти. Он не используется в приведенном выше коде, однако его можно использовать для передачи размера объекта, который должен быть выделен диспетчеру памяти.
  • На слайде выше показан возможный сценарий, в котором базовый класс, реализующий необходимые подпрограммы управления памятью, наследуется двумя дочерними классами, которые не обязательно связаны между собой. Проблема в том, что, поскольку new и delete неявно статичны, любой член, к которому они обращаются, также должен быть статическим. В нашем случае, если класс диспетчера памяти использует некоторый элемент в качестве хранилища памяти, этот член, в свою очередь, также должен быть статическим, и это приведет к тому, что любой класс, наследуемый от нашего класса диспетчера памяти, будет использовать тот же член, что и его хранилище — не обязательно то, что нам нужно.

    Шаблоны и проблема статического члена

    Как описано выше, простое переопределение new и delete на уровне класса в лучшем случае предоставят специальное решение из-за описанного выше ограничения статического члена. Однако есть способ создать обобщенную версию этого решения — мы можем использовать шаблоны для преодоления этого ограничения.

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

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

    Совместное использование шаблонов и переопределений new и delete

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

Заключение

Я надеюсь, что этот метод будет полезен всем, кто оказался в той же ситуации, в которой оказался я — когда количество изменений, к которым приводили другие решения, было неприемлемо. Однако я также считаю, что это не единственная причина использовать именно этот подход. Я считаю, что он предлагает больший контроль над кодом, чем другие, более традиционные подходы, такие как размещение new или какой-то статический метод создания, который потенциально может позволить вам достичь аналогичного эффекта. Кроме того, это позволит вам повторно использовать код во многих различных объектах и ​​менеджерах памяти.

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

// declare a class A to use the allocation proxy class
class A : public  CAllocationProxy<MemoryManager, A> {
  // implement class...
}

// using class A

// initialize proxy instance...
A::InitProxyInstance();

// all allocations are now done by the proxy... 
A myA = new A(); 
 ....
// deletion is performed by the proxy as well...
delete myA;

Спасибо за чтение!

// undef any new macros
#ifdef new
#define __new new
#undef new
#endif

// undef any delete macros
#ifdef delete
#define __delete delete
#undef delete
#endif 
....
// rest of code here
 ...
// restore new macros
#ifdef __new
#define new __new
#undef __new
#endif

// restore delete macros
#ifdef __delete
#define delete __delete
#undef __delete
#endif

Первоначально опубликовано на сайте «chronodrones.blogspot.com» 20 марта 2016 г.

Прокси-распределение объектов с помощью шаблонов и новых/удаляемых переопределений на уровне класса в C++