Шаблоны C и выделение заголовков

#c #templates #memory #allocation

#c #шаблоны #память #распределение

Вопрос:

Недавно я столкнулся с проблемами с выделением памяти, выполненным в одной DLL (или *.so — portable code), и освобождением, выполненным в другой DLL. Ошибки, с которыми я столкнулся до сих пор, являются:

  • Это просто не работает — сбой assert () при отладке.
  • Это не сработает, если одна DLL была статически связана со стандартной библиотекой C, а другая DLL была динамически связана с ней.
  • Это не сработает, если одна DLL выполняет выделение, затем DLL выгружается, а другая DLL пытается освободить эту память.

В принципе, правило, которому я решил следовать, заключается в том, чтобы не создавать выделения в одной DLL и не выпускать ее в другой (и предпочтительно хранить ее в одном cpp-файле). Обычно это также означает, что я не должен выполнять выделение в заголовочном файле, который может совместно использоваться более чем одной библиотекой DLL. Это означает, что я не должен выполнять выделения во временных файлах (поскольку все они находятся в заголовке), и это довольно большое ограничение.

Когда мне действительно нужно создать новый объект в шаблоне, то, что я делаю сейчас, это выделяю для него в памяти cpp-файл и только затем запускаю его c’torс помощью оператора размещения new.

 // header
class MyBase
{
public:
  static void* allocate(std::size_t i_size);
};

template <typename T>
class MyClass: MyBase
{
public:
  T* createT();
};

temlpate <typename T>
T* MyClass<T>::createT()
{
  void* pMem = MyBase::allocate( sizeof(T) );
  return new (pMem) T;
}

// Cpp file
void* MyBase::allocate(std::size_t i_size)
{
  return malloc( i_size );
}
  

Хотя это работает, это немного некрасиво. Это означает написание шаблонного кода без использования new.

Другим следствием является то, что если вы не знаете, что шаблон был написан с использованием этой техники, вы должны использовать только его методы const в файле заголовка (включая другие шаблоны) (это предполагает, что методы const не выделяют и не освобождают память). Сюда входит STL. На самом деле, одно из мест, где я столкнулся с этим, было в векторе, размер которого был изменен одной динамической библиотекой (на HP-UX), а затем выгружен, чем его предшественник, вызванный другой динамической библиотекой.

Есть ли какое-то широко известное решение для этого, которого мне просто не хватает, или это просто упущенная из виду проблема?

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

1. «Это не работает» — это НЕ описание ошибки.

2. @Tomalak Geret’kal: Отладочные версии завершаются ошибкой при выполнении assert(). Выпуск версии иногда завершается сбоем в SIGSEGV.

3. Правильно, поэтому опубликуйте обратную трассировку из SIGSEGV и текст ошибки утверждения.

4. проблема каким-то образом решается в boost::shared_ptrs; возможно, вам следует взглянуть на их реализацию

Ответ №1:

В принципе, правило, которому я решил следовать, заключается в том, чтобы не создавать выделения в одной DLL и не выпускать ее в другой (и предпочтительно хранить ее в одном cpp-файле). Обычно это также означает, что я не должен выполнять выделение в заголовочном файле, который может совместно использоваться более чем одной библиотекой DLL.

Нет, одно не подразумевает другое.

Если ваши функции выделения и отмены выделения являются шаблонами в заголовке, это все равно нормально; просто убедитесь, что вы ограничили использование этих функций для любого данного объекта одним TU1.

Инкапсулируйте ваши объекты таким образом, чтобы для кода в DLL 1 было недопустимо / запрещено / неопределенно вызывать эти функции для объектов из DLL 2. Оформите это контрактом с пользователем, напишите в комментариях, что право собственности на объекты остается за исходным контекстом выделения, затем переходите к следующей части вашего проекта, никогда больше не беспокоясь об этом.

То, что функции доступны для всех TU, не имеет значения; в конце концов, вы всегда можете попытаться delete использовать эти вещи!


1единица перевода. Примерно эквивалентно одному предварительно обработанному .cpp файлу.

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

1. @Tomalak Geret’kal — можете ли вы продемонстрировать «Инкапсулируйте свои объекты таким образом, чтобы для кода в DLL 1 было недопустимо / запрещено / неопределенно вызывать эти функции для объектов из DLL 2»?

2. @selalerer: Прочитайте второе предложение того же абзаца.

3. @Tomalak Geret’kal — Есть ли такая документация у всех соответствующих классов в проектах, над которыми вы работаете? Или все люди в проекте знают, что это неявное ограничение для всех классов, если не указано иное?

4. @selalerer: Это задокументировано. Функции без такой документации имеют неоднозначное право собственности на объекты и являются подозрительными. Это те, на разгадку которых вам придется потратить время и, возможно, провести рефакторинг.

5. @Tomalak Geret’kal: Я думаю, что решение в документации немного проблематично. Я работаю в проекте, в котором было и остается много разработчиков, не все из них заслуживают достаточного доверия, чтобы я мог рассчитывать на то, что они прочитают комментарии, поймут их и будут следовать им в точности. Спасибо, что подтвердили, что проблема существует. Я думаю, что буду продолжать искать решение, с которым я буду чувствовать себя более комфортно.

Ответ №2:

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

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

1. Я думаю, что эта информация верна конкретно для Windows, но не обязательно для другой комбинации OS arch.