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

Если вы читаете еще один вопрос об aliasing (Что такое строгое правило псевдонимов?) и его главный ответ, я понял, что все еще не полностью удовлетворен, хотя я думаю Я все это понял.

(Этот вопрос теперь помечен как C и С++. Если ваш ответ относится только к одному из них, уточните, пожалуйста.)

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

(Обновление: конечно, мы могли бы просто избегать всех видов пиннинга, но это не очень учебное. Если, конечно, не существует четких ноль исключений, кроме исключения union. )

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

Это оригинальный пример:

int main()
{
   // Get a 32-bit buffer from the system
   uint32_t* buff = malloc(sizeof(Msg));

   // Alias that buffer through message
   Msg* msg = (Msg*)(buff);

   // Send a bunch of messages    
   for (int i =0; i < 10; ++i)
   {
      msg->a = i;
      msg->b = i+1;
      SendWord(buff[0] );
      SendWord(buff[1] );   
   }
}

Важная строка:

Msg* msg = (Msg*)(buff);

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

Msg* msg = (Msg*)(buff);
msg->a = 5;           // writing to one of the two pointers
SendWord(buff[0] );   // renders the other, buffer, invalid

Поэтому мое предлагаемое правило заключается в том, что после создания второго указателя (т.е. create msg) вы должны немедленно и навсегда "удалить" другой указатель.

Какой лучший способ убрать указатель, чем установить его в NULL:

Msg* msg = (Msg*)(buff);
buff = NULL; // 'retire' buff. now just one pointer
msg->a = 5;

Теперь последняя строка, назначающая msg->a, не может аннулировать любые другие указатели, потому что, конечно, их нет.

Далее, конечно, нам нужно найти способ вызова SendWord(buff[1] );. Это невозможно сделать немедленно, потому что buff был удален и равен NULL. Мое предложение теперь - отбросить назад.

Msg* msg = (Msg*)(buff);
buff = NULL; // 'retire' buff. now just one pointer
msg->a = 5;

buff = (uint32_t*)(msg);   // cast back again
msg = NULL;                // ... and now retire msg

SendWord(buff[1] );

Итак, каждый раз, когда вы накладываете указатель между двумя "несовместимыми" типами (я не уверен, как определить "несовместимый"?), вы должны немедленно "удалить" старый указатель. Установите его в NULL явно, если это поможет вам применить правило.

Насколько достаточно консервативен?

Возможно, это слишком консервативно и имеет другие проблемы, но я сначала хочу знать, достаточно ли это достаточно консервативно, чтобы не вводить UB, нарушая строгий псевдоним.

Наконец, повторите исходный код, измененный для использования этого правила:

int main()
{
   // Get a 32-bit buffer from the system
   uint32_t* buff = malloc(sizeof(Msg));

   // Send a bunch of messages    
   for (int i =0; i < 10; ++i)
   {  // here, buff is 'valid'

      Msg* msg = (Msg*)(buff);
      buff = NULL;
      // here, only msg is 'valid', as buff has been retired
      msg->a = i;
      msg->b = i+1;
      buff = (uint32_t*) msg;  // switch back to buff being 'valid'
      msg = NULL;              // ... by retiring msg
      SendWord(buff[0] );
      SendWord(buff[1] );
      // now, buff is valid again and we can loop around again
   }
}

Ответ 1

Ответ на С++: это не сработает. Строковое правило сглаживания С++ явно перечисляет, какие типы могут использоваться для доступа к объекту. Если вы используете другой тип, вы получаете UB, даже если вы "удалили" все методы доступа другого типа. Согласно С++ 14 (n4140) 3.10/10, допустимыми типами являются:

Если программа пытается получить доступ к сохраненному значению объекта через значение gl, отличное от одного из следующие типы: undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим стандартной версии динамического типа cv объекта,
  • совокупность или тип объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических члены данных (в том числе, рекурсивно, элемент или нестатический элемент данных для суммирования или содержит объединение),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char.

"Подобные типы", согласно 4.4, относятся к модификации cv-квалификации многоуровневых указателей.

Итак, если вы когда-либо записывали в область с помощью указателя (или другого доступа) к одному типу, вы не можете получить к нему доступ через указатель на другой тип (если не санкционировано 3.10/10), даже если вы забудете старый указатель.

Если вы никогда не писали область в определенном типе, указатели на кастинг туда и обратно не являются проблемой.

Ответ 2

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

Пока вы не получаете доступ к указателю типа, а другой, "официальный", в порядке. Однако, если вы это сделаете, это приведет к поведению undefined, которое может просто работать, сделать то, что вы сказали или что-то из этой галактики, в том числе сделать другой указатель недействительным. Компиляторы могут обрабатывать UB по своему усмотрению.

Единственный способ сделать buff допустимым указателем на Msg - memcpy/memmove, в соответствии со стандартом:

memcpy( (void*)msg, (const void*) buff, sizeof (*msg));

Кроме того, какие триггеры UB не только записывают, но и читают или каким-либо другим способом, который обращается к объекту:

Если программа пытается получить доступ к сохраненному значению объекта через значение l, отличное от одного из следующих типов , поведение undefined

Некоторые компиляторы также позволяют "приостанавливать" это правило, такое как GCC, clang и ICC (возможно, также MSVC), но это нельзя считать переносимым или стандартным поведением. Дальнейшие методы и анализ их генерации кода тщательно анализируются здесь.

Вам действительно нужно нарушить правило строгого сглаживания?

В большинстве случаев нет, вам это не нужно. Существует способы и способы решения этой проблемы, которые включают в себя совершенно законные решения. В приведенном выше случае просто сохраните простой указатель внутри struct и отправьте каждый член в определенном формате.

Ответ 3

Правило:

"Если указатели не совместимы, вы не можете иметь два указывающие на одну и ту же память".

Вот более простой пример бесконечного цикла:

1: int *some_buff = malloc(sizeof(whatever));
2: memset(some_buff,0,sizeof(whatever));
3: while (some_buff[0] == 0)
4: {
5:     whatever *manipulator = (whatever*)some_buff; 
6:     manipulate(manipulator);
7: }

Это по существу то, как компилятор будет/может подходить к этому коду:

Тест для some_buff[0] == 0 можно оптимизировать, потому что не является допустимым способом изменения some_buff[0]. это доступ через manipulator, но manipulator не является совместимого типа, поэтому в соответствии с правилом строгого сглаживания, значение some_buff[0] не может измениться.

Если вам нужен еще более простой пример:

int *some_buff = malloc(sizeof(whatever));
memset(some_buff,0,sizeof(whatever));
whatever *manipulator = (whatever*)some_buff;
manipulate(manipulator);
printf("%d\n",some_buff[0]);

Совершенно нормально, чтобы этот код всегда печатал ноль, и не имеет значения, что делает манипуляция.

Ответ 4

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

Для C ( не для С++) существует, по крайней мере, одна безопасная вещь, кроме как избежать произвольной записи: вы можете смело набрасывать указатели на структуры, учитывая, что один тип структуры просто добавляет поля до конца другого. Это даже работает, когда более длинная структура просто содержит более короткий, чем ее первый член: указатель на структуру указывает на ее первый член. Так, например, они безопасны в C:

typedef struct
{
    int id;
    const char *name;
} base_t;

typedef struct
{
    base_t base;
    long foo;
} derived_t;

derived_t *d = malloc(sizeof derived_t);
base_t *b = (base_t *)d;
int *i = (int *)d;