Предположим, что вам нужно несколько т.н. "сигнальных" значений. К примеру, пусть ваша функция работает с указателями на тип Widget, а вам требуется способ передавать значения с особым смыслом, к примеру: "Не указан Widget", или "Используй значение по умолчанию", или "Возьми Widget из родительского объекта", или "Все известные Widget-ы".
Ну, почти любой язык уже даёт вам как минимум одно значение, которое можно использовать в качестве сигнального: это
nil
, null
, Nothing
, nullptr
или аналогичное "пустое" значение в вашем языке. Если вам нужно только одно значение, то вот вам простой вариант.С другой стороны, это "пустое" значение является настолько общим, что оно может быть получено по ошибке. К примеру, это значение может быть значением по умолчанию для указателей. Значение может поступить от предыдущей (неудачной) операции (вроде выделение или поиска). Вы можете захотеть использовать какое-то другое значение, чтобы по ошибке не перепутать его с такими случаями.
1). Если вам нужно небольшое число сигнальных значений, то вы можете просто выделить несколько объектов-заглушек, адрес которых вы будете использовать в качестве значения, не используя при этом сами объекты.
2а). Другая идея состоит в создании частного региона памяти и использовании указателей из него. Вы можете вызвать VirtualAlloc(MEM_RESERVE) для резервирования региона. В таком регионе никто не сможет выделить память/создать объекты. Если вы зарезервируете регион и целенаправленно не будете его использовать, то любой адрес из этого региона можно использовать в качестве сигнального значения.
2б). Аналогичную работу делает за вас и Windows: при инициализации адресного пространства процесса ядро резервирует нижние 64 Кб адресного пространства, чтобы в них нельзя было выделить память. Это даёт вам 65'536 потенциальных значений (ну, одно из них занято
nil
, поэтому у вас появляется только 65'535 новых значений). В примеру, эта техника используется макросами MAKEINTRESOURCE
и MAKEINTATOM
для передачи числовых значений вместо строк.До Windows 8 у приложений был способ "разрезервировать" этот регион памяти в 64 Кб и начать выделять в нём память - представьте себе хаос, к которому это может привести! В счастью, в Windows 8 это уже невозможно.
3). Если ваши Widget имеют требования по выравниванию (а если они состоят из чего угодно, кроме простого массива байт - то, вероятно, такие требования имеются), вы можете использовать любой указатель, который не удовлетворяет этим требованиям. К примеру, если Widget - это указатель (к примеру, в Delphi любой объект является указателем), то он, следовательно, должен быть выровнен на границу 4 байт (в 32-битной программе). Таким образом, любое значение указателя, не делящееся нацело на 4, может быть использовано в качестве сигнального значения - поскольку такое значение никогда не будет закреплено за допустимым объектом.
Примечание переводчика: строго говоря, если объект Delphi выделяется стандартным образом (через стандартный менеджер памяти), то он будет выровнен как минимум на границу 8 байт - это характеристика менеджера памяти Delphi, закреплённая в официальной документации. Опционально вы можете переключить менеджер памяти в режим выравнивания на 16 байт.
Даже если требования к выравниванию всего только два байта (например, запись с полями типа Word и меньше), то даже это даст вам два миллиарда значений (все нечётные 32-битные значения) - что очень много. Вы можете использовать
f(n) := n * 2 + 1
, чтобы создать сигнальное значение по номеру и g(n) := (n − 1) div 2
, чтобы преобразовать сигнальное значение в его номер.А если вы используете 64-битные указатели, то число возможных сигнальных значений просто ошеломляет.
Упражнение: Что не так с этим советом: "Возьмите любое значение большее $80000000"?
Лол, только что прочитал в оригинальном блоге (вы меня подсадили).
ОтветитьУдалитьЯ как-то делал велосипед для сериализации «старых» объектов по TypeOf, но с возможностью работать с произвольными типами, идентифицируя их фиктивными TypeOf. И в качестве такого фиктивного TypeOf использовал указатель на любой метод объекта:
const Type_Vec3: pointer = @Vec3.GetLength;
...
RegisterType(Type_Vec3, @SerializeVec3, @DeserializeVec3);
...
Serialize(vec, Type_Vec3).
В FPC это работает, но может сломаться, если однажды компилятор научат объединять одинаковые функции (внезапно тоже было, https://blogs.msdn.microsoft.com/oldnewthing/20050322-00/?p=36113). Вот бы их как-нибудь последовательно распределять в compile-time, начиная с pointer(1).
Хотя сейчас подумал, что можно использовать уникальные строковые константы, приведённые к указателям — они «наверное точно» не пересекутся ни друг с другом, ни с настоящими VMT.
> вы меня подсадили
УдалитьТак было задумано :)
> в качестве такого фиктивного TypeOf использовал указатель на любой метод объекта
Эээ... перекомпилируем программу, меняются адреса - что раньше сохранили, теперь не прочитать. Не?
Нет, они только для идентификации типа в пределах программы, как и «настоящие» TypeOf, а в поток сохраняются под именами и/или порядковыми номерами, конечно.
Удалить"sentinel" в качестве существительного "часовой" выглядит нормально, а вот как прилагательное - не очень (например, по заголовку совершенно непонятно, о чём речь). Может быть, "охранный, сигнальный"?
ОтветитьУдалитьПоменял на "сигнальное", спасибо.
УдалитьУ дяди 1-апреля что ли?
ОтветитьУдалитьПосту больше подходит название
"Вредные советы от бывалых или как ковыряться в зубах дулом пистолета"
И судя по тамошнему диалогу - народ в восторге!
Ну, это смотря как программить. Чен пишет для нормальных программеров.
Удалить