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

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

Поясните пожалуйста, зачем поступать столь варварским способом вместо того чтобы глобально переопределить new и delete?

Если говорить о том, что где-то в C++ программе кто-то использует malloc/free вместо new/delete… Ну в угол такого автора надо поставить и конфет лишить.
new и delete — да, malloc и free — нет.
Большинство полезных сторонних библиотек написаны на C, а значит new и delete, очевидно, там не используются.
Простой пример — sqlite.
Ну и я бы не назвал этот способ варварским. Обрезанная библиотека создается один раз и далее просто используется. Кроме того, использовать ее можно только в конфигурации Release, а в Debug все оставить как было.
Зачем глобально переопределять new и delete, когда это можно сделать для отдельных классов, которые часто аллокируется?
Ну это вопрос скорее к автору, который решил во всей программе поменять всю работу с динамической памятью. Стандартный способ для C++ это сделать — переопределить new/delete глобально.

Для гибрида или чистого C действительно только и остается, что подменить стандартную библиотеку.

Однако, при наличии исходников можно поиском/заменой переименовать все malloc/free на что-то свое.

Я с вами согласен, уместнее было бы выявить узкое место, и там подменить аллокатор, а не затевать крупномасштабные маневры с компоновщиком.
> Однако, при наличии исходников можно поиском/заменой переименовать все malloc/free на что-то свое.
А вот это как раз варварский способ. Этим мы сразу сильно усложняем себе дальнейшее обновление используемых библиотек.
Хорошо, если библиотека позволяет подменять аллокатор (например pnglib), а если не позволяет, то подмена стандартной библиотеки видится мне единственным способом.
НЛО прилетело и опубликовало эту надпись здесь
А зачем? В С malloc определяется в mem.h и stdlib.h. Можно вместо них подключить что-то своё.
А можно через линкер символы переопределить.
Например malloc и free используются при разработке собственных контейнеров. Да, кстати std::aallocator тоже не использует new и delete (только placement)
Проблемы, которые можно получить, используя malloc/free:
  • malloc возвращает void*, который затем приходится силком преобразовывать в нужный тип. При этом, контроль над размером и типом возлагаются на программиста;
  • malloc тяжело переопределить штатными средствами, из-за чего собственно и приходится идти на разные ухищрения вроде описанных автором;
  • если перепутать и попытаться высвободить память, выделенную malloc, при помощи delete, получим неизвестно что;
  • используя malloc, можно позабыть вызвать конструктор.

Ссылки:
  1. Ответ на stackoverflow
  2. Ответ на C++ FAQ
  3. Заметка на cppreference

Все эти источники гласят: в чистом C++ malloc/free скорее вредны. Единственное оправдание — работа с гибридным C/C++ кодом.
Покажите другой кроссплатформенный способ реализовать нормально тот же аналог std::vector. А эти проблемы из разряда «что-бы нанятая мартышка ничего не сломала»
А что не так с std::vector? Он использует абстрактный аллокатор, который имеет только методы allocate/deallocate. Иными словами, всякий раз, когда вектору надо подрасти (.size()>.capacity()), он выделяет новый блок и копирует (C++11 — перемещает) в него все элементы.

Иллюстрации процесса на stackoverflow

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

realloc программисту C++ не друг, а источник плавающих ошибок.
Тогда по другому: напишите свою кроссплатформенную реализациб std::allocator, идентичную оригиналу, не используя malloc/free
Так я в ней буду использовать стандартные операторы из C++ — new и delete. А уж как их реализовали авторы стандартной библиотеки — меня не волнует.
И получите тормоза из-за ненужных вызовов конструкторов, выделяя память с запасом, когда можно просто выделить сырую память malloc и вызывать конструктор отдельно для уже выделенного участка памяти, когда потребуется. Именно так работает весь STL, и именно тем отличаются у контейнеров там reserve() от resize().
Давайте уточним, во-первых, о какой реализации STL мы говорим. Я говорю о реализации GNU STL, которая используется в GCC.

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

Так каким образом, по вашему, STL реализует resize/reserve? При помощи malloc/realloc или тем, который по вашему заверению «дает тормоза»?
Стандартный аллокатор в реализация STL в gcc вызывает не malloc, а "::operator new(size)", как раз для того, чтобы можно было переопределить этот new глобально для всего приложения.
В реализации STL в Visual Studio 2012 стандартный аллокатор тоже вызывает "::operator new(size)", а не malloc. В реализациях Apache stdcxx и libc++ делается то же самое.

Вообще странно бы было, если бы стандартный аллокатор пытался бы вызывать malloc, когда есть возможность вызвать new и предоставить возможность его глобального переопределения.
Глянул в стандарт. Пункт 20.9.5.1 требует, чтобы стандартный аллокатор использовал ::operator new для получения памяти.
Уточню — работа с динамической памятью в C++ должна осуществляться посредством new/delete.

Если при этом мы желаем воспользоваться наследием C, каким-нибудь fake_alloc, мы должны обернуть его в new/delete, с учетом стандарта не забыть обеспечить выброс исключений, и обращаться к вызовам fake_alloc только из этих самых new/delete.
В allocate вызывается new char[size], в deallocate — delete [] reinterpret_cast<char*>(ptr). Зачем malloc/free?
НЛО прилетело и опубликовало эту надпись здесь
Я правильно понял, что ваша версия new
void * __cdecl operator new(size_t sz)
{
     return dlmalloc(sz);
}

игнорирует стандарт C++, § 5.3.4/15:
[Note: unless an allocation function is declared with a non-throwing exception-specification(15.4), it indicates failure to allocate storage by throwing a std::bad_alloc exception (Clause 15, 18.6.2.1); it returns a non-null pointer otherwise. If the allocation function is declared with a non-throwing exception-specification, it returns null to indicate failure to allocate storage and a non-null pointer otherwise.—end note]

[Замечание: За исключением ситуации, когда функция, выделяющая память, объявлена не выбрасывающей исключений(15.4), индикация ошибки выполняется путем выбрасывания исключения std::bad_alloc; в случае отсутствия ошибок, функция должна выдать ненулевой указатель. Если же таковая функция объявлена, как не бросающая исключений, она должна вернуть ноль для индикации ошибки при выделении памяти, и ненулевой указатель в противном случае. — конец замечания]

Вы подменяете глобальный new, поэтому вы должны, чтобы не вызвать непоняток у вышестоящих библиотек, бросать исключение при ошибке выделения.
Формально вы, конечно, правы.
Я выложил код, который использую у себя. Так получилось, что в своих программах я предпочитаю исключения C++ отключать вообще. Я сторонник такого подхода, что таких исключений в программе быть не должно, краши же ловятся с помощью SEH.
Что же касается исключения по причине нехватки памяти, то, согласитесь, в современных системах памяти столько, что ситуация, когда программе ее не хватило, говорит о какой-то фундаментальной проблеме приложения. В любом случае bad_alloc — это в 99% случаев гарантированное падение.
Резюмируя:
да, по хорошему я должен был написать в коде бросок исключения. Вообще я должен был написать еще много чего, например проверку на то, в C или C++ файл включают мою заглушку, удобный способ замены dlmalloc'а на что-то еще, комментариев побольше, и т.п.
Это всё так, но всё дело в том, что у меня не было такой задачи. Я не задавался целью написать универсальную заглушку на все случаи жизни. Я задавался целью познакомить программистов со способом подмены аллокатора. Просто воспринимайте этот код в качестве… м… учебного, чтоли.
Если и считать ваш код учебным, то только в качестве примера «как делать не следует». Опасно вольничать со стандартом. Очень сомнительно игнорировать языковые механизмы (интересно, как вы реализуете конструктор без исключений? Как реализуете RAII?).
Как работать с библиотеками, которые информируют об ошибках только при помощи исключений (STL, кстати, не исключение)? Как, наконец, корректно вычищать ресурсы после возникновения проблем? Объявлять панику и падать?

Ссылка на интересную запись в блоге Nicholas Nethercote об опасности кода, использующего стандартную библиотеку C++, и скомпилированного без поддержки исключений.

В любом случае, ваша замена — глобальна, следовательно — влияет на все вызовы new в программе. Что будет происходить, когда new вместо выбрасывания исключения вернет нуль, а программа, справедливо не ожидающая такого подвоха, его разыменует? UB будет происходить. Любой код, содержащий любые, даже маловероятные (закон Мерфи, как известно, работает), предпосылки к UB может быть отправлен исключительно в мусорку. (Если это не очередная иллюстрация — «смотрите, а еще UB можно сотворить вот так»).
учебный код в данном случае — не «то как надо делать», а то что «сделано упрощенно, чтобы только понять суть».
Исключения при выделении памяти вещь на практике довольно сомнительная. По сути в user space на современных десктоп платформах все что может сделать приложение при получении ошибки выделения памяти — это завершить работу, что можно сделать либо явно (abort) или через segfault по обращению к нулевому указателю. Сделать какой-то recovery в случае ошибки выделения — это удел каких-нибудь специализированных приложений или где-нибудь в embedded/kernel (где исключения скорей все не используются в любом случае).
Это очень сильно завязано на реализацию аллокатора в ОС. И это может быть пользовательский аллокатор поверх своего, пользовательского пула памяти.
И тогда ситуация «пул заполнен» не означает «у ОС кончилась память».

А еще, в такой ситуации приложение может попытаться начать повторный расчет, используя более экономный метод. Язык дает автору все средства для этого.
В этом примере нет ничего про пул, заменяется стандартный new/malloc. Если какая-то из библиотек использует пул и специализированный аллокатор, то они в логике ничего не должны потерять.

Реалистично приложениям отработать ошибку работы с памятью в портабельном C++ невозможно. Допустим на linux вы пишете char* hello = new char[очень_большое_число], то hello получить валидный указатель поскольку Linux не выделяет физическую память в этот момент, и программа упадет из-за нехватки памяти в момент записи в какой-нибудь hello[i].

Если я правильно понял вашу отсылку на стандарт, то чтобы этот new из crtfunc.h соответствовал стандарту ему нужно добавить non-throwing exception-specification или внутри проверять на null и выбрасывать bad_alloc? Я думаю это несложно было бы сделать и соответстовать стандарту, хоть никаким лучшим образом на поведение программ это бы не повлияло.
А если память сильно фрагментирована и такого блока просто нет? Если у меня C++ на микроконтроллере работает, и при недостатке памяти я просто игнорирую всю дальнейшую обработку и жду, пока память освободится?

Вы хотите сказать, что в стандарте вообще надо было проигнорировать ошибки, связанные с памятью, потому как вам лично кажется, что из таких ошибок восстановиться невозможно?
Нет, я говорю что данной библиотеке можно было бы вполне проигнорировать обработку ошибок от аллокатора по стандарту потому какого-либо способа ее обработки кроме terminate нет. (читаем статью: VC2013, Windows).
Игнорирование этой ошибки приводит к неопределенному поведению (показано выше). Неопределенное поведение недопустимо.
Если вы под неопределенным поведением понимаете разыменовывание нулевого указателя, то хотя по стандарту C++ оно неопределено, на ОС Windows у него вполне конкретное проявление при чтении/или записи. Так что здесь мы по сути выбираем между terminate() или EXCEPTION_ACCESS_VIOLATION. Решать разработчику приложения важна ли ему разница.

Я думаю что конечно лучше было бы привести к стандрту как вы предлагаете: это просто, код можно скопировать на другие платформы, да и terminate() — это лучше чем access violation. Кроме того исключение может освободит какие-то ресурсы.
Что-то как-то все сложно. Не знаю, как работает линковщик в MSVC, но, например, в gcc он поступает довольно прямолинейно — ищет символ сначала в объектных файлах, а если не находит, то затем во всех библиотеках по порядку, пока не найдет. Соответственно, кто первый нашелся — того и тапки.

Тогда чтобы подменить какой-то символ, надо всего лишь сделать так, чтобы он оказался в пути поиска раньше стандартной библиотеки. Например, скомпилировать свою версию malloc и линковать проект с соответствующим объектным файлом:

test.cpp
#include <cstdlib>
int main()
{
	return (size_t)malloc(112);
}


my_malloc.c
#include <stdlib.h>

void * malloc(size_t sz)
{
	return (void*)42; //cannot print from here, as printf or puts can use malloc inside
}


compile & run
cc -c -Wall my_malloc.c
g++ -c -Wall test.cpp
g++ test.o my_malloc.o -o test
./test
echo $?
Честно говоря, я уже и не помню, почему я просто не поменял порядок линковки. Возможно потому что линковщик от ms всегда ищет сначала в своих библиотеках. Не могу точно сказать. Может просто плохо пытался. Эксперименты с подменой я начал проделывать еще на 2008-й студии и, помню, тогда ее линковщик выдавал предупреждение на появление дублей. Может быть это предупреждение показалось мне тогда неприемлемым. А может просто хотелось избавиться от стандартного аллокатора с гарантией (все же порядок линковки не кажется мне надежной штукой, т.к. обычно все должно линковаться в любом порядке с одинаковым результатом).
В конце концов, эти вещи из разряда «блажь перфекциониста». Скорее всего работа программы без полной замены аллокатора ничем не будет отличаться от той же программы с полной заменой. Однако хочется идеала, даже если его никто не видит кроме тебя.
Не секрет, что стандартный аллокатор new/delete/malloc/free в языке C/C++ не блещет быстродействием. Конечно, всё зависит от реализации, но, если говорить об оной от компании Microsoft, то это факт.

Факт ли? Вот например создатель ned malloc утверждает, что в современных OS системные аллокаторы очень хороши.

To my knowledge, nedmalloc is among the fastest portable memory allocators available, and it has many features and outstanding configurability useful in themselves. However it cannot consistently beat the excellent system allocators in Windows 7, Apple Mac OS X 10.6+ or FreeBSD 7+ (and neither can any other allocator I know of in real world testing).
www.nedprod.com/programs/portable/nedmalloc/

И кому в данном случае верить: создателю одного из альтернативных аллокаторов или Вашим голословным утверждениям, которые сделаны для рационализации сомнительных велосипедов?
Абсолютли. Я ожидал увидеть сравнения по быстродействию, но увы.
Вполне допускаю, что для частной задачи автора его специализированный аллокатор будет быстрее. Но скорее всего он не будет так же универсален, как системный.
Проблемму фрагментации памяти в Windows решает технология Low-Fragmentation Heap, которая включена по умолчанию начиная с Vista, а в XP включается через WinAPI
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории