Comments 20
Однако можно легко столкнуться с ситуацией, когда указатель лишь иногда бывает null
ЕМНИП, на AIX, `malloc(0)` возвращал `NULL`.
Поскольку по POSIX `free(NULL)` - норма жизни, это не вызывало принципиальных проблем. В общем, отрадно, что они до этого добрались, и иногда сужают области этих новомодных UB.
С malloc(0)==NULL случай интересный, это implementation-defined поведение, которому есть разумное объяснение.
Бывает полезно, когда malloc(0) возвращает то же, что и malloc(1) - это позволяет написать обобщённый код, одновременно выделяющий память под структуру (в том числе нулевого размера) и использующий указатель в качестве уникального идентификатора в какой-то другой структуре данных.
Прошу прощения, всем известно, что нультерминированые строки - тормоза, каких мало. И вот был код, работал спокойно, многими десятилетиями:
void mstrinit(mstr_t *mstr, const char *cstr) {
mstr.size = strlen(cstr);
mstr.ptr = malloc(mstr.size);
memcpy(mstr.ptr, cstr, mstr.size);
}
Но тут понабежали адепты UB имени последнего, то ли draft, то ли С23. Вот чего ради, даже этот абсолютно корректный код пытаться обозвать UB?
Ну, да, `memcpy(NULL, NULL, 0)`, и что? Последние 50 лет никому не мешал, на всех платформах, и тут здравствуй дерево. Ну, это так, умозрительный пример, в реалиях, используется не malloc()
в хардкоде, а ссылка на функцию оптимального аллокатора для данного приложения, и т.д..
И подобных вариантов использования - легион.
И слава Богу, что им хоть на этом последнем рубеже, надавали по щам и отправили в пешее эротическое... Подозреваю, если чуть-чуть призадуматься и копнуть хоть на сантиметр, то окажется, что 2/3 новомодных UB имеет похожую природу.
Явно, в комитет нужен кто-то типа, или Т..., или П..., т.к. "повесточка" давно вышла из под контроля.
Ну как абсолютно корректный.. Однажды я захотел намеренно вызвать segfault и написал разыменование ноля. Компилятор выкинул опкод присвоения, будто его и не было, а в значении переменной оказалось вообще значение соседнего регистра. И это было достаточно неожиданно.
Забавное поведение компилятора. Хотя, конечно, при работе с NULL длины 0 разыменование не происходит, но эти "забавы" пугают.
И, как видим, многих пугают, если "рекомендовано реализовать изменение ретроактивно". Похоже, новые стандарты на компиляторы, способны сделать хороший код ошибочным и/или опасным.
NULL - NULL как возвращающее 0
мне кажется зря это пытаются определить.
И считаю правильным, что для memXXX null это UB. В любом случае в них не должен null попадать. Есть же пропозалы на nullability аттрибуты, и это позволит в compile-time проверять, да и код станет чище и быстрее без лишних проверок.
без лишних проверок.
Ви так говорите, как будто это что-то хорошее.
Да, это - быстродействие
это — быстродействие
Ви так говорите, как будто Ви куда‑то настолько торопитесь, что Вам 3 ГГц — и тех не хватает.
А по‑хорошему «[библиотечный] метод должен принимать любые значения, включая потенциально ошибочные».
По хорошему должно быть 2 библиотечных метода:
1) принимает любые значения, проверяет их на ошибки.
Название короткое и удобное.
2) принимает любые значения, не проверяет их на ошибки (UB)
Длинное неудобное название (натипа xyz_unchecked), и требуется явное указание компилятору, что ты всё делаешь правильно.
По хорошему, у методов должны быть две точки входа - основная и "быстрая". А между ними должны располагаться пролог с проверками входных данных и ранними return-ами (как в данном случае, если размер 0 то вернуть ок).
Пролог должен быть частью определения функции и доступен компилятору вызывающего кода.
Для неявных вызовов (по указателю, например) - используется основная точка входа.
Для явных, когда функция и её пролог точно известны на момент компиляции, компилятор вызывающего кода может заинлайнить пролог и проверить, не приводит ли это к выкидыванию части проверок. Например, если мы проверили размер на ноль где-то выше, то в ветке где 0 мы можем вообще выкинуть вызов memXYZ. А в ветке где не ноль, то можем выкинуть вторую проверку и прыгать сразу в быструю точку входа.
Согласен.
Но у программиста все-равно должен быть способ прыгнуть сразу в "быструю" точку входа, т.к. компилятор не всегда может выкинуть проверки.
Например, сейчас я пишу реализацию кольцевого буфера. И в методе write
проверяю что размер входных данных (count
) меньше size_t / 2
. Размер буфера тоже меньше size_t / 2
.
Это значит что любые проверки на переполнение для head + count
и tail + count
можно выкинуть, но компилятор об этом никак не догадается.
Если компилятор гарантирует, что переменная не может быть NULL, то зачем тратить ресурсы на проверку того, что невозможно? Ресурсы не только у cpu, но и написание кода, добавление теста на бранч с null и т.п.
Насчет библиотечных методов частично соглашусь, так как предполагаю, если в стандарт занесут нулябельность, то c либами будут нюансы. И в них в публичных методах придется проверять NULL. Но библиотеки это еще не все программирование.
Если компилятор гарантирует, что переменная не может быть NULL, то зачем тратить ресурсы на проверку того, что невозможно?
Современные компиляторы достаточно умные для того, чтобы на этапе компиляции выкинуть эту проверку как «всегда false и потому никогда не исполняющуюся».
"нулябельность" там была, почти всегда. Это ж ещё надо постараться, что б найти проблему с тем, что у чего-то указатель NULL, если его длина 0. Это больше общий случай, нежели частный.
Но неконтролируемое расширение толкования UB сверх всяких разумных мер, и в этом месте, тоже потребует кода для выделения этого частного случая (NULL длины 0) из обобщённого кода работы.
Работает - не трогай. Тем более, что это ничего полезного не принесёт.
Хм, а это ничего, что в стандарте C написано "`malloc()` ... If size is zero, the behavior of malloc is implementation-defined. For example, a null pointer may be returned. Alternatively, a non-null pointer may be returned; but ..." ?
Если работу с NULL длины 0 объявить UB, то потребуется проанализировать прорву наследованного кода на этот предмет, и добавить кучу проверок на предмет длины 0 с обходом. О каком быстродействии может идти речь, при необходимости доработок такого рода?
Впрочем, и в новом коде, обход работы с NULL длины 0 только добавит всяких `if`. К примеру, если malloc(0)
будет возвращать не 0, а 42, то и free(ptr)
должно будет сравнивать ptr не только с 0, но и 42. И т.д. и т.п.
И это только первый контрпример, который сразу вспомнился лично мне. Реально их гораздо больше.
Поясню, почему NULL - NULL == 0
идея весьма сомнительная.
Потому что в ОС нулевыми считаются указатели не только с адресом 0, а вообще с любыми малыми адресами. В частности, на amd64 в большинстве ОС нулями считаются все адреса меньше 2Мб, а на солярке - меньше 4 Гб. ОС тупо не выделяют физическую память под эти адреса, а 2 Мб — это размер "средней" страницы в амд64.
Зачем это надо? Ну потому что запись вида x[3].y
вполне себе даст ненулевой адрес, даже если x == 0
.
В Си NULL это константа, имеющее одно значение так или иначе.
NULL + 1 пройдёт любую проверку на NULL, так как x == NULL способен сравнить только с одним значением.
Компилятору пофиг, какие зарезервированные адреса у разных ОС (да и не отличаются принципиально зарезервированные адреса от просто ещё не спроецированных, кроме того что зарезервированные точно не выдаст тебе системный аллокатор). Это чисто мера runtime перестраховки такая же как и, например, рандомизация кучи. Учитывать это не требуется в 99% программ. И компилятор тоже не учитывает в кодогенерации.
NULL + 1 пройдёт любую проверку на NULL,
Зависит от стандарта и компилятора. Сейчас скорее всего да, но так было не всегда. Скажем, в солярке 20 лет назад проверка на NULL выполнялась только для старших 32 бит адреса, независимо от младших, и это было явно определено в руководстве к штатному компилятору. Да и сам NULL официально приравняли к ((void*)0) опять же относительно недавно (позже, чем завезли nullptr в плюсы)
Избавляемся от UB в memcpy