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

Проблема глобального переопределения new/delete в C++/CLI

Время на прочтение 4 мин
Количество просмотров 6.3K
Как известно, C++ позволяет глобально переопределять операторы new и delete. Обычно такое переопределение используется для диагностики, поиска утечек памяти и более эффективного распределения памяти.

Все это мы используем в нашем крупном проекте. Однако у нас есть часть, написанная на C#, которая с помощью C++/CLI взаимодействует с основной частью на C++. И вот тут появились проблемы. У нас получались утечки памяти там, где их быть ну никак не могло.

Все сводилось к тому, что вызывался наш new, но delete был crt’шный.
Чтобы разобраться с проблемой, я создал тестовый solution, где смоделировал ситуацию. Для простоты переопределенные функции выглядят так:
void* __cdecl operator new( size_t size )
{
 printf("New\n");
 return malloc(size);
}

void __cdecl operator delete( void *p )
{
 printf("Delete\n");
 free(p);
}


Значит, мы должны получить одинаковое количество New и Delete в выводе, равное количеству пар new/delete.

Вот код, который вызывает new/delete:
//managed.h
namespace Managed
{
 public ref class ManagedClass
 {
  public:
   ManagedClass()
   {
    NativeClass* cl = new NativeClass();
    delete cl;
    int* a = new int();
    delete a;
   }
 };
}

//native.cpp
NativeClass::NativeClass()
{
 int* a = new int();
 delete a;
}


Из C# мы создаем ManagedClass так:
static void Main( string[] args )
{
 ManagedClass cl = new ManagedClass();
}


Еще есть класс Foo, который лежит в отдельном файле basic.h, причем он никак не используется в этих классах. Просто его определение помещается в Precompiled Header.
Этот “плохой” класс не несет функциональности, однако одно наличие его приводит к очень странному результату. Если комментируешь конструктор копирования класса, то все работает отлично:
#pragma once

class Foo
{
public:
 Foo() {}
 //Foo( const Foo& ) {}
 Foo& operator = ( const Foo& ) { return *this; }

 virtual ~Foo() {}
};


Вывод:
   New
   New
   Delete
   Delete
   New
   Delete

3 new – 3 delete. Все хорошо.

Если же конструктор копирования не закомментирован, то не всем нашим new соответствует наш delete:
#pragma once

class Foo
{
public:
 Foo() {}
 Foo( const Foo& ) {}
 Foo& operator = ( const Foo& ) { return *this; }

 virtual ~Foo() {}
};


Вывод:
   New
   New
   Delete
   New

3 new – 1 delete. Все плохо.

Итог: мы имеем ошибку выбора функции удаления, которая зависит от “положения луны”. Самое время начать гуглить.

В ходе поисков был найден документ, предположительно описывающий нашу проблему:
http://support.microsoft.com/kb/122675.

К сожалению, после детального прочтения документа, выясняется, что наш случай отличается от рассмотренного. Более того, в pure-C++ проекте такого поведения не наблюдается. Проблема в C++/CLI.

Продолжим наши исследования. Если деструктор сделать не виртуальным, то опять все работает, как надо:
#pragma once

class Foo
{
public:
 Foo() {}
 Foo( const Foo& ) {}
 Foo& operator = ( const Foo& ) { return *this; }

 ~Foo() {}
};


Вывод:
   New
   New
   Delete
   Delete
   New
   Delete

Стоит заметить, что этот класс никак не используется в нашей программе. Просто само его наличие приводит к багу.
Т.к. баг проявляется только в C++/CLI, то попробуем пометить этот класс как unmanaged:
#pragma managed(push,off)
#include "../System/basic.h" // h файл с “плохим” классом Foo
#pragma managed(pop)


Вывод:
   New
   New
   Delete
   Delete
   New
   Delete

Получилось! Однако, это не решение проблемы. Будем копать дальше.

Может быть проблема в Precompiled Headers? Однако если basic.h из Stdafx.h, но вставить его в любой другой *.h файл, который попадает в проект, то проблема повторится.

Посмотрим более подробно, что делает linker. Для этого включим режим вывода дополнительной информации у linker’а:
image

Ого! Он нашел delete в MSVCRTD.lib, вот почему delete используется не наш:
image

В случае же, когда мы делаем деструктор не виртуальным или комментируем конструктор копирования, получаем такой выход linker’а:
image

Более того, если оставить конструктор копирования, сделать деструктор не виртуальным, но добавить виртуальную функцию, опять delete начинает глючить.

Хотя исследования были в Debug сборке, но и в Release наблюдается то же самое поведение.

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

В итоге имеем, что при наличии класса, у которого есть виртуальная таблица и конструктор копирования (причем этот класс компилируется в managed), linker линкует стандартный delete, хотя у нас и есть переопределенный delete в dll.

PS. Кому интересно самостоятельно поработать с простейшим тестовым solution с проблемой и, возможно, помочь советом, могут скачать его вот здесь: https://mysvn.ru/Lof/NewDeleteProblem
Теги:
Хабы:
+27
Комментарии 20
Комментарии Комментарии 20

Публикации

Истории

Работа

QT разработчик
13 вакансий
Программист C++
122 вакансии

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн