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

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

(Этот вопрос теперь помечен как C и 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

Поэтому предлагаемое мной правило состоит в том, что, как только вы создадите второй указатель (т.е. создадите 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
   }
}

person Aaron McDaid    schedule 15.07.2015    source источник
comment
Практическое правило: не набирайте текст. Некоторые его экземпляры четко определены, но большинство - нет, и, если нет чертовски веской причины, вы обычно можете написать более красивое решение, не использующее каламбур.   -  person fuz    schedule 15.07.2015
comment
@FUZxxl, в некотором смысле согласен. Мне никогда не приходилось этого делать, и сейчас мне не нужно этого делать. Но мне любопытно. В какой-то момент в моей жизни у меня может не быть выбора, кроме как немного раздвинуть границы. Если все говорят, что не делайте этого, или это всегда UB, тогда у меня не будет другого выбора, кроме как просто кодировать это и сказать моему боссу, что он сработал в моих тестах, поэтому мне придется работать с ним. потому что я не могу получить других полезных советов :-).   -  person Aaron McDaid    schedule 15.07.2015
comment
возможный дубликат Что такое строгое правило псевдонима?   -  person this    schedule 15.07.2015
comment
... если, конечно, действительно не существует никогда какой-либо версии каламбура с определенным поведением. У меня складывается впечатление, что это может иметь место конкретно в C ++, поскольку в этом случае он может быть намного строже, чем C.   -  person Aaron McDaid    schedule 15.07.2015
comment
Я знаю, что некоторые люди отмечают это как ложный вопрос. Но я упомянул именно этот вопрос в первых нескольких словах своего вопроса, именно для того, чтобы подчеркнуть, что у меня есть другой, дополнительный вопрос. Этот другой вопрос меня не интересует.   -  person Aaron McDaid    schedule 15.07.2015
comment
@AaronMcDaid Ну, вы просили «простейшее практическое правило», я дал вам простейшее практическое правило.   -  person fuz    schedule 15.07.2015
comment
Кстати. если вы хотите сделать код так, чтобы он не нарушал строгого псевдонима, это другой вопрос, и процесс относительно прост.   -  person Šimon Tóth    schedule 15.07.2015
comment
Я хотел бы написать подробный анализ вашего кода, однако очень важно определить, является ли uint32_t typedef для unsigned int в вашей системе. Если нет, то ваш код явно UB. Если так, то мутно (Но ИМХО, не УБ). Я предлагаю обновить ваш вопрос так, чтобы для Msg::a использовался явно тот же тип, что и для buff[0] и т. Д., Либо использовались явно разные типы. Ответ на оба случая был бы слишком длинным.   -  person M.M    schedule 15.07.2015
comment
Кроме того, двойная маркировка этого вопроса усложняет его вдвойне, потому что правило строгого псевдонима в C отличается от C ++. Фактически, в C ++ очень недооценено то, что происходит при наложении псевдонимов в malloc'd пространстве.   -  person M.M    schedule 15.07.2015
comment
Я не помечал его двойными тегами. Сначала я пометил его как C ++, но, должно быть, кто-то еще добавил C. Оглядываясь назад, я больше заинтересован в C.   -  person Aaron McDaid    schedule 16.07.2015
comment
@MattMcNabb, я скопировал этот пример прямо из другого ответа SO, и я не осознавал, что проблема связана с подписью.   -  person Aaron McDaid    schedule 16.07.2015
comment
Подписанность @AaronMcDaid не является проблемой, вопрос в том, являются ли эти два типа одинаковыми или нет. Код, из которого вы скопировали, имеет ту же проблему. Пожалуйста исправьте.   -  person M.M    schedule 16.07.2015


Ответы (4)


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

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

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

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

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

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

person Angew is no longer proud of SO    schedule 15.07.2015
comment
Если вы используете другой тип, вы получите UB. Это кажется настолько широким, что писать весь набор текста на C ++ как UB? Неужто что-то разрешено? Думаю, я возражаю против слова «использовать» здесь, поскольку оно довольно расплывчатое. Считает ли начальное приведение использованием. Или вам нужно читать / писать через приведенный указатель. - person Aaron McDaid; 15.07.2015
comment
Действительно тупой короткий пример, поясняющий мой предыдущий комментарий: int main() { long x = 5; long *xp = &x; int *ip = (int*) xp; *ip = 4; } Это UB? - person Aaron McDaid; 15.07.2015
comment
Обоснование строгого правила наложения имен обычно дается с точки зрения оптимизации, предполагающей, что определенные указатели / значения l указывают на разные данные. Удалив один указатель, я устраняю эту проблему и не допускаю никаких несоответствующих оптимизаций, потому что компилятор больше не может выполнять некорректные оптимизации. Итак, строгое алиасинг все еще остается проблемой, хотя я обошел часто приводимую причину этого правила ?! - person Aaron McDaid; 15.07.2015
comment
@AaronMcDaid Если у вас есть два указателя с несовместимыми типами, они фактически restrict указатели. Таким образом, компилятор не обязан обновлять данные, полученные от одного указателя к другому. - person Šimon Tóth; 15.07.2015
comment
@Aaron: Разрешен каламбур союзного типа. Если вам нужно набрать каламбур, вот как это сделать. - person Kevin; 15.07.2015
comment
@AaronMcDaid Да, ваш пример _1 _ + _ 2_ - UB в соответствии со стандартом C ++. Я не могу говорить за К. - person Angew is no longer proud of SO; 15.07.2015
comment
@Agnew: Тогда почему он в опубликованном вами списке? - person Kevin; 15.07.2015
comment
@ Кевин Хорошо, может ты и прав. Кажется, я всегда неверно истолковывал этот пункт. - person Angew is no longer proud of SO; 15.07.2015
comment
@Let_Me_Be, я никогда не предполагал, что он обязан когда-либо обновлять данные, видимые с одного указателя на другой. Мой пример строго построен, чтобы избежать каких-либо предположений о том, может ли один указатель видеть, что произошло, через другой указатель. - person Aaron McDaid; 15.07.2015
comment
@AaronMcDaid В вашем примере эту строку buff = (uint32_t*) msg; можно безопасно оптимизировать, поскольку ни buff, ни msg не изменились. - person Šimon Tóth; 15.07.2015
comment
@Let_Me_Be, эта строка не может быть удалена - она ​​действительно вносит изменения. buff имеет значение NULL перед этой строкой и отличное от NULL после этой строки. Это важно, потому что это означает, что разыменование баффа вскоре после (buff[0]) лучше определено, потому что buff теперь не равно нулю. - person Aaron McDaid; 15.07.2015
comment
В C ++ неясно, что это за тип malloc'd пространства. В C есть правило, согласно которому запись в malloc'd пространство накладывает отпечаток на тип выражения, выполняющего запись, но в C ++ такого правила нет. Также в C ++ неясно, вызывает ли копирование объектов в malloc'd пространство начало жизни объекта в malloc'd пространстве. (Стандарт не говорит, что это так) - person M.M; 15.07.2015
comment
@AaronMcDaid buff = NULL;, конечно, тоже пойдет. Что у вас есть: установите для баффа значение null; бафф не используется; установить бафф на исходное значение. Это можно полностью оптимизировать. - person Šimon Tóth; 15.07.2015

Правило такое:

«Если только указатели не совместимы. У вас не может быть двух указателей, указывающих на одну и ту же память».

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

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]);

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

person Šimon Tóth    schedule 15.07.2015
comment
Для ясности: два несовместимых указателя, указывающих на одну и ту же память, в порядке. Это зависит от того, что вы делаете, что эти указатели относительно того, вызываете ли вы UB. Но вы можете использовать это как практическое правило, чтобы избегать любых возможных проблем. - person M.M; 15.07.2015
comment
У вас не может быть двух указателей, указывающих на одну и ту же память. Да, ты можешь. - person edmz; 15.07.2015
comment
@black И чем это отличается от предыдущего комментария? - person Šimon Tóth; 15.07.2015
comment
@Let_Me_Be Немного, но вы еще не исправили это в своем ответе, что вы можете сделать. - person edmz; 15.07.2015
comment
@black Вопрос о практическом правиле. Ни определение, ни спецификация. Это хорошее правило, позволяющее избежать проблем, даже если оно запрещает допустимые случаи. - person Šimon Tóth; 15.07.2015
comment
Я думаю, что петля сбивает с толку. Развернем петлю. Тогда строки кода (считая, как в этом ответе в настоящее время): 1,2,3,5,6,3,5,6,3,5,6,3,5,6, .... Когда точно станет UB? Если бы программа была только 1,2,3, она была бы определена? А как насчет 1,2,3,5? 1,2,3,5,6? 1,2,3,5,6,3? 1,2,3,5,6,3,5? Мое текущее мнение таково, что 1,2,3,5,6 - это DB (определенное поведение), но 1,2,3,5,6,3 - это UB. Если вы согласны, у меня есть важный дополнительный вопрос. Спасибо! - person Aaron McDaid; 16.07.2015
comment
@AaronMcDaid Если вы развернете цикл, вы получите 1,2,5,6,5,6,5,6,5,6,5,6 .... Тест никогда не будет оцениваться. Обсуждение того, является ли 1,2,3,5,6 или 1,2,3,5,6,3 UB, бессмысленно, поскольку код интерпретируется иначе. - person Šimon Tóth; 16.07.2015
comment
@AaronMcDaid Кстати. Я думаю, что основное заблуждение, которое вы не можете понять, заключается в том, что когда дело доходит до строгого псевдонима, неопределенное поведение - это не то, что происходит во время выполнения, оно проявляется во время компиляции. - person Šimon Tóth; 16.07.2015
comment
@Let_Me_Be, у меня нет такого заблуждения. Мой вопрос: если мы возьмем строки 1,2,3,5,6 и поместим их в программу самостоятельно и скомпилируем эту короткую программу, это будет UB или нет? - person Aaron McDaid; 16.07.2015
comment
@AaronMcDaid Да, но, скорее всего, этого не произойдет. Если бы условие было some_buff[0] != 0, оно проявилось бы как 1,2,3,5,6 стало бы 1,2,3. - person Šimon Tóth; 16.07.2015
comment
Ваша редакция (даже более простой пример) идеальна. Что, если мы удалим последнюю строку (printf("%d\n",some_buff[0]);) и заменим ее на int *another_buff = (int*) manipulator; printf("%d\n",another_buff[0]);? Если это БД (определенное поведение), то мне это очень интересно. (И спасибо за терпение!) - person Aaron McDaid; 16.07.2015
comment
@AaronMcDaid Это все еще неопределенное поведение, хотя фактическое поведение компиляторов должно быть относительно согласованным. Но может случиться, например, следующее: memset(); int value = some_buff[0]; .... int *another_buff = (int*)manipulator; printf("%d\n",another_buff[0]); в этом я могу определенно увидеть компилятор, использующий кешированное значение из value. - person Šimon Tóth; 16.07.2015
comment
В вашем втором примере я согласен, что value будет кэшироваться. Но это не меняет моего понимания моего int *another_buff примера. (...продолжение следует...) - person Aaron McDaid; 16.07.2015
comment
Давайте продолжим это обсуждение в чате. - person Šimon Tóth; 16.07.2015
comment
Может, нам стоит немного отступить. Память memset до нуля. Затем есть whatever *manipulator = (whatever*)some_buff; и manipulate(manipulator);. «Знает» ли функция manipulate, что данные были обнулены ранее? Если да, то как он имеет право об этом знать. Если нет, то почему мы обнуляем его? - person Aaron McDaid; 16.07.2015

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

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

Единственный способ сделать buff действительным указателем на Msg - это _3 _ / _ 4_ в соответствии со стандартом:

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

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

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

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

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

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

person edmz    schedule 15.07.2015
comment
@Angew Да, конечно (я действительно говорю о доступе к объекту). По крайней мере, мы это понимаем. - person edmz; 15.07.2015
comment
Я не понимаю первую строчку этого вопроса. Вы говорите, что если я пишу через указатель с типовой карамелью, то я получаю UB, даже если я больше никогда не читаю и не пишу через любой указатель? - person Aaron McDaid; 15.07.2015
comment
@Angew Под допустимым я подразумеваю безопасный доступ, а именно, что указанный адрес действителен. Чтобы инициализировать эту область, вы должны скопировать содержимое объекта MsgBuf таким образом, чтобы сглаживание не было проблемой; char*, например, как, вероятно, memcpy. Это помогает? - person edmz; 15.07.2015
comment
@AaronMcDaid Да, исправляем. Он обращается к объекту через указатель с типом, вызывающий UB. Как говорит Энгью, простое указание на него не вызывает никаких проблем. - person edmz; 15.07.2015
comment
Извините за педантизм, но вы не можете открыть его с определенным UB, а затем укажите исключение (memcpy), которое снова делает его нормальным. Если memcpy в порядке, то можем ли мы дистиллировать почему все в порядке? Если бы я заново реализовал свой memcpy, тогда было бы хорошо, да? И memcpy имеет void*, а не char* в своей подписи, поэтому тот факт, что char* является особенным, не имеет отношения к оправданию memcpy. (Извините, если мой тон здесь неправильный, но это важно.) - person Aaron McDaid; 15.07.2015
comment
Извините, перепутал memcpy в содержании и указателе, игнорируйте мои предыдущие комментарии. - person Angew is no longer proud of SO; 15.07.2015
comment
@AaronMcDaid Абсолютно нормально. memcpy должен вычислять байтовую копию n байтов от dst до src. void* используется для представления адреса, а C позволяет неявное преобразование в void* из других типов указателей (в отличие от char*); внутренне src и dst конвертируются правильно. - person edmz; 15.07.2015
comment
Адреса, на которые указывают параметры memcpy, не должны перекрывать друг друга. Но в примере, который мы используем, они действительно перекрывают друг друга, потому что один указатель - это просто приведение другого указателя. Вы имеете в виду другой пример, где две структуры данных не перекрываются? - person Aaron McDaid; 15.07.2015
comment
@AaronMcDaid Это зависит от того, когда вы это называете. После того, как вы malloc uint32_t*, они не перекрываются и memcpy можно безопасно вызывать; OTOH, когда они перекрываются, вам понадобится memmove, как указано в ответе. - person edmz; 15.07.2015
comment
@black, они не пересекаются. Мы даже не можем спросить, пересекаются ли они сейчас - потому что msg даже не существует! Я думаю, вы обсуждаете ситуацию, когда buff и msg были malloc редактированы по отдельности? - person Aaron McDaid; 15.07.2015
comment
Другими словами, ваш ответ в том виде, в каком он написан, подразумевает, что мы можем просто скопировать и вставить memcpy( (void*)buff, (const void*) msg, sizeof (msg)); в исходный (проблемный) код, и это исправит его? - person Aaron McDaid; 15.07.2015
comment
@AaronMcDaid Да (посмотрите на правку, все наоборот). Будет ли msg содержать значимые значения, зависит от фактических данных в buff. - person edmz; 15.07.2015
comment
@black, теперь я опубликовал собственный ответ, в котором memmove агрессивно используется для решения проблемы. Любая обратная связь приветствуется. - person Aaron McDaid; 20.07.2015
comment
И, возвращаясь к вашему ответу, @black. Я проголосовал против из-за использования memcpy. Эту единственную строку нельзя вставить в мою программу, потому что память перекрывается и memcpy не может использоваться с перекрывающейся памятью. В любом случае, я полагаю, вы имели в виду sizeof(*msg) вместо sizeof(msg). - person Aaron McDaid; 21.07.2015
comment
@AaronMcDaid Не стесняйтесь делать это; но это не повод для отрицательного голосования imho. Если память перекрывается, используйте memmove, который предназначен только для этого. - person edmz; 21.07.2015

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

Для C (не для 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;
person Community    schedule 15.07.2015
comment
Я не утверждаю, что установка указателя на NULL на самом деле имеет какое-то значение для компилятора. Это делается с целью напомнить мне больше не использовать этот указатель. И поскольку это вынуждает меня прекратить использование этого указателя, я не могу получить доступ к тому же участку памяти через указатели несовместимых типов. Или, точнее, я не обращаюсь к одному и тому же местоположению с двумя разными одновременно. Существует четкая «передача», так сказать, в одном месте кода (приведение) - person Aaron McDaid; 16.07.2015
comment
... вполне возможно, что в течение жизненного цикла программы из-за malloc и free у вас будет два разных указателя двух разных типов, указывающих на одно и то же место. И это явно не проблема. Следовательно, проблема может быть только в том, что два указателя существуют одновременно. Я думаю. - person Aaron McDaid; 16.07.2015