Насколько портативный использует нижний бит указателя в качестве флага?

Если есть, например, класс, для которого требуется указатель, и bool. Для простоты в примерах будет использоваться указатель int, но тип указателя не имеет значения, если он указывает на то, что size() больше 1.

Определение класса с членами { bool , int *} приведет к тому, что класс имеет размер, который вдвое больше размера указателя и много потерянного пространства

Если указатель не указывает на char (или другие данные size(1)), то предположительно младший бит всегда будет равен нулю. Класс может быть определен с помощью {int *} или для удобства: union { int *, uintptr_t }

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

Определенный способ:

struct myData
{
 int * ptr;
 bool flag;
};
myData x;

// initialize
x.ptr = new int;
x.flag = false;

// set flag true
x.flag = true;

// set flag false
x.flag = false;

// use ptr
*(x.ptr)=7;

// change ptr
x = y;                // y is another int *

И предлагаемый способ:

union tiny
{
 int * ptr;
 uintptr_t flag;
};
tiny x;

// initialize
x.ptr = new int;

// set flag true
x.flag |= 1;

// set flag false
x.flag &= ~1;

// use ptr
tiny clean=x;      // note that clean will likely be optimized out
clean.flag &= ~1;  // back to original value as assigned to ptr
*(clean.ptr)=7;

// change ptr
bool flag=x.flag;
x.ptr = y;             // y is another int *
x.flag |= flag;

Это похоже на поведение undefined, но насколько переносимым является это?

Ответы

Ответ 1

Пока вы восстанавливаете младший бит указателя до того, как пытаетесь использовать его в качестве указателя, он, вероятно, будет "разумно" переносимым, если ваша система, ваша реализация C++ и ваш код соответствуют определенным предположениям.

Я не могу обязательно дать вам полный список предположений, но в верхней части моей головы:

  • Предполагается, что вы не указываете ни на что, чей размер составляет 1 байт. Это исключает char, unsigned char, signed char, int8_t и uint8_t. (И это предполагает CHAR_BIT == 8; в экзотических системах, скажем, с 16-битными или 32-битными байтами, другие типы могут быть исключены.)
  • Предполагается, что объекты размером не менее 2 байтов всегда выровнены по четному адресу. Обратите внимание, что x86 не требует этого; Вы можете получить доступ к 4-байтовому int по нечетному адресу, но это будет немного медленнее. Но компиляторы обычно организуют хранение объектов по четным адресам. Другие архитектуры могут иметь другие требования.
  • Предполагается, что указатель на четный адрес имеет младший бит, установленный в 0.

Для этого последнего предположения у меня есть конкретный контрпример. В векторных системах Cray (J90, T90 и SV1 - те, которые я использовал сам), машинный адрес указывает на 64-битное слово, но компилятор C под Unicos устанавливает CHAR_BIT == 8. Байтовые указатели реализованы в программном обеспечении, с 3-битным байтовым смещением в слове, которое хранится в неиспользуемых старших 3 битах 64-битного указателя. Таким образом, указатель на 8-байтовый выровненный объект может легко иметь младший бит равным 1.

Были реализации Lisp (пример), которые используют младшие 2 бита указателей для хранения тега типа. Я смутно помню, что это вызывало серьезные проблемы при портировании.

Итог: вы, вероятно, можете избежать неприятностей с большинством систем. Будущие архитектуры в значительной степени непредсказуемы, и я легко могу представить, как ваша схема сломается в следующем Большом Новом.

Некоторые вещи для рассмотрения:

Можете ли вы хранить логические значения в битовом векторе вне вашего класса? (Поддержание связи между указателем и соответствующим битом в битовом векторе оставлено в качестве упражнения).

Попробуйте добавить код ко всем операциям с указателями, которые завершаются неудачно с сообщением об ошибке, если он когда-либо видит указатель с битом младшего разряда, установленным в 1. Используйте #ifdef, чтобы удалить проверочный код в рабочей версии. Если вы столкнетесь с проблемами на какой-либо платформе, создайте версию своего кода с включенными проверками и посмотрите, что произойдет.

Я подозреваю, что по мере роста вашего приложения (оно редко сжимается) вы захотите хранить больше, чем просто bool вместе с указателем. Если это произойдет, проблема с пространством исчезнет, потому что вы все равно уже используете это дополнительное пространство.

Ответ 2

В "теории": это поведение undefined, насколько я знаю.

В "реальности": он будет работать на повседневных машинах x86/x64 и, возможно, на ARM тоже?
Я не могу сделать выражение выше этого.

Ответ 3

Он очень портативный, и, кроме того, вы можете assert, когда принимаете необработанный указатель, чтобы убедиться, что он соответствует требованию выравнивания. Это будет застраховано от непостижимого будущего компилятора, который каким-то образом запутывает вас.

Только причины не делать этого - это расходы на чтение и общее обслуживание, связанные с такими "хакерскими" вещами. Я бы уклонился от него, если бы не было четкой выгоды. Но иногда это стоит того.

Ответ 4

Соответствует этим правилам, и он должен быть очень портативным.