Что такое строгое правило наложения имен?

805

Отвечая на вопрос о распространенном неопределенном поведении в C , люди иногда ссылаются на строгое правило псевдонимов.
О чем они говорят?

Benoit
источник
12
@Ben Voigt: правила псевдонимов различны для c ++ и c. Почему этот вопрос помечен cи c++faq.
MikeMB
6
@MikeMB: Если вы проверите историю, вы увидите, что я сохранил теги такими, какими они были изначально, несмотря на попытки некоторых экспертов изменить вопрос из-под существующих ответов. Кроме того, зависимость от языка и версии является очень важной частью ответа на вопрос «Что такое правило строгого наложения имен?» и знание различий важно для команд, которые переносят код между C и C ++ или пишут макросы для использования в обоих.
Бен Фойгт
6
@Ben Voigt: На самом деле - насколько я могу судить - большинство ответов касаются только c, а не c ++, а формулировка вопроса указывает на акцент на C-правилах (или OP просто не знал, что есть разница ). По большей части правила и общая идея, конечно же, одинаковы, но особенно там, где профсоюзы заинтересованы, ответы не относятся к c ++. Я немного обеспокоен тем, что некоторые программисты на С ++ будут искать строгое правило псевдонимов и будут просто предполагать, что все изложенное здесь относится и к с ++.
MikeMB
С другой стороны, я согласен с тем, что изменить вопрос после публикации множества хороших ответов проблематично, и в любом случае проблема незначительна.
MikeMB
1
@MikeMB: Я думаю, вы увидите, что C, сфокусированный на принятом ответе, что делает его некорректным для C ++, был отредактирован третьей стороной. Эта часть, вероятно, должна быть пересмотрена снова.
Бен Фойгт

Ответы:

562

Типичная ситуация, когда вы сталкиваетесь со строгими проблемами псевдонимов, - это наложение структуры (например, сообщения устройства / сети) на буфер размера слова вашей системы (например, указатель на uint32_ts или uint16_ts). Когда вы накладываете структуру на такой буфер или буфер на такую ​​структуру с помощью приведения указателя, вы можете легко нарушить строгие правила наложения имен.

Таким образом, при такой настройке, если я хочу отправить сообщение чему-либо, мне нужно иметь два несовместимых указателя, указывающих на один и тот же кусок памяти. Затем я мог бы наивно кодировать что-то вроде этого (в системе с sizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // 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]);   
    }
}

Строгое правило псевдонимов делает эту настройку недопустимой: разыменование указателя на псевдоним объекта, который не является совместимым типом или одним из других типов, разрешенных C 2011 6.5, пункт 7 1, является неопределенным поведением. К сожалению, вы все еще можете кодировать таким образом, возможно, получить несколько предупреждений, сделать так, чтобы он нормально компилировался, только для того, чтобы иметь странное неожиданное поведение при запуске кода.

(GCC выглядит несколько непоследовательным в своей способности давать псевдонимы предупреждениям, иногда давая нам дружеское предупреждение, а иногда нет).

Чтобы понять, почему это поведение не определено, нам нужно подумать о том, какое правило строгого алиасинга покупает компилятор. По сути, с этим правилом не нужно думать о вставке инструкций для обновления содержимого buffкаждого запуска цикла. Вместо этого, при оптимизации с некоторыми досадно необоснованными предположениями о псевдонимах, он может пропустить эти инструкции, загрузить buff[0]и buff[1] в регистры ЦП один раз перед запуском цикла, и ускорить тело цикла. До того, как был введен строгий псевдоним, компилятор должен был жить в состоянии паранойи, содержимое которого buffможет измениться в любое время и в любом месте кем-либо. Таким образом, чтобы получить дополнительное преимущество в производительности, и при условии, что большинство людей не печатают указатели, введено строгое правило псевдонимов.

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

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

И переписал наш предыдущий цикл, чтобы воспользоваться этой удобной функцией

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Компилятор может или не может быть достаточно умным, чтобы попытаться встроить SendMessage, и он может или не может решить загружать или не загружать бафф снова. Если SendMessageявляется частью другого API, который скомпилирован отдельно, он, вероятно, содержит инструкции для загрузки содержимого баффа. С другой стороны, возможно, вы находитесь в C ++, и это некая шаблонная реализация только для заголовков, которую компилятор считает, что она может быть встроенной. Или, может быть, это просто то, что вы написали в своем .c файле для вашего удобства. В любом случае неопределенное поведение все еще может возникнуть. Даже когда мы знаем о том, что происходит под капотом, это все равно является нарушением правила, поэтому не гарантируется четко определенное поведение. Так что простое включение в функцию, которая принимает наш буфер с разделителями слов, не обязательно поможет.

Так как мне обойти это?

  • Используйте союз. Большинство компиляторов поддерживают это, не жалуясь на строгий псевдоним. Это разрешено в C99 и явно разрешено в C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Вы можете отключить строгое псевдонимы в вашем компиляторе ( f [no-] strict-aliasing в gcc))

  • Вы можете использовать char*для псевдонимов вместо слова вашей системы. Правила допускают исключения для char*(в том числе signed charи unsigned char). Всегда предполагается, что char*псевдонимы других типов. Однако это не сработает по-другому: нет предположения, что ваша структура псевдоним буфера символов.

Начинающий остерегаться

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

сноска

1 Типы, которые C 2011 6.5 7 разрешает доступ к lvalue:

  • тип, совместимый с эффективным типом объекта,
  • квалифицированная версия типа, совместимого с эффективным типом объекта,
  • тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,
  • агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или
  • тип персонажа.
Дуг Т.
источник
17
Я иду после битвы, кажется ... может unsigned char*быть использован далеко char*вместо? Я склонен использовать, unsigned charа не charв качестве базового типа, byteпотому что мои байты не подписаны, и я не хочу, чтобы странность поведения со знаком (особенно в отношении переполнения)
Matthieu M.
30
@Matthieu: Подпись не имеет значения для правил псевдонимов, так что использование unsigned char *в порядке.
Томас Эдинг
22
Разве это не неопределенное поведение при чтении из члена профсоюза, отличного от последнего, в который был записан?
Р. Мартиньо Фернандес
23
Черт, этот ответ полностью задом наперед . Пример, который он показывает как незаконный, на самом деле является законным, а пример, который он показывает как законный, на самом деле является незаконным.
Р. Мартиньо Фернандес
7
Ваше uint32_t* buff = malloc(sizeof(Msg));и последующие unsigned int asBuffer[sizeof(Msg)];объявления буферов объединения будут иметь разные размеры, и ни один из них не будет правильным. mallocВызов полагается на выравнивании 4 байта под капотом (не делать) , а объединение будет в 4 раза больше , чем это должно быть ... Я понимаю , что это для ясности , но это ошибка мне ни-the меньше ...
бездушный
233

Лучшее объяснение, которое я нашел, - Майк Актон, « Понимание строгого алиасинга» . Он немного сфокусирован на разработке PS3, но в основном это только GCC.

Из статьи:

«Строгий псевдоним - это предположение, сделанное компилятором C (или C ++), что разыменование указателей на объекты разных типов никогда не будет ссылаться на одну и ту же ячейку памяти (то есть псевдонимы друг друга).»

Таким образом, в основном, если у вас есть int*указатель на некоторую память, содержащую, intа затем вы указываете float*на эту память и используете ее как floatнарушающую правило. Если ваш код не соблюдает это, оптимизатор компилятора, скорее всего, сломает ваш код.

Исключением из правила является a char*, которому разрешено указывать на любой тип.

Найл
источник
6
Так какой же канонический способ легально использовать одну и ту же память с переменными 2 разных типов? или все просто копируют?
jiggunjer
4
Страница Майка Актона имеет недостатки. По крайней мере, часть «Пролить через союз (2)» совершенно неверна; Кодекс, который он утверждает, является законным, не является.
Давмак
11
@davmac: авторы C89 никогда не предполагали, что это заставит программистов прыгать через обручи. Я нахожу совершенно странным представление о том, что правило, существующее с единственной целью оптимизации, следует интерпретировать таким образом, чтобы требовать от программистов писать код, который избыточно копирует данные в надежде, что оптимизатор удалит избыточный код.
Суперкат
1
@curiousguy: "Не может быть союзов"? Во-первых, первоначальная / основная цель профсоюзов никоим образом не связана с алиасами вообще. Во-вторых, спецификация современного языка явно разрешает использовать объединения для псевдонимов. Компилятор обязан заметить, что используется объединение, и обработать ситуацию особым образом.
до
5
@curiousguy: Ложь. Во-первых, оригинальная концептуальная идея, лежащая в основе союзов, заключалась в том, что в любой момент в данном объекте объединения есть только один объект-член, «активный», тогда как другие просто не существуют. Таким образом, нет «разных объектов по одному адресу», как вы, кажется, верите. Во-вторых, псевдонимы нарушений, о которых все говорят, это доступ к одному объекту как к другому объекту, а не просто наличие двух объектов с одинаковым адресом. Пока нет доступа к типу , нет проблем. Это была оригинальная идея. Позже было разрешено печатать на профсоюзах.
AnT
133

Это строгое правило псевдонимов, которое можно найти в разделе 3.10 стандарта C ++ 03 (другие ответы дают хорошее объяснение, но ни один из них не содержит самого правила):

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

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

Формулировки C ++ 11 и C ++ 14 (изменения подчеркнуты):

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

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

Два изменения были небольшими: glvalue вместо lvalue и прояснение случая совокупности / объединения.

Третье изменение дает более сильную гарантию (ослабляет строгое правило псевдонимов): новая концепция похожих типов , которые теперь безопасны для псевдонимов.


Также формулировка C (C99; ISO / IEC 9899: 1999 6.5 / 7; точно такая же формулировка используется в ISO / IEC 9899: 2011 §6.5 ¶7):

Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов 73) или 88) :

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

73) или 88) Целью этого списка является определение тех обстоятельств, при которых объект может или не может быть псевдонимом.

Бен Фойгт
источник
7
Бен, поскольку людей часто направляют сюда, я позволил себе добавить ссылку на стандарт Си также для полноты картины.
Кос
1
Посмотрите на обоснование C89 cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf раздел 3.3, в котором об этом говорится.
phorgan1
2
Если кто-то имеет lvalue типа структуры, берет адрес члена и передает его функции, которая использует его в качестве указателя на тип члена, это будет рассматриваться как доступ к объекту типа члена (Legal), или объект типа структуры (запрещено)? Во многих кодах предполагается, что доступ к структурам таким образом разрешен, и я думаю, что многие люди будут вопить о правиле, которое понималось как запрещение таких действий, но неясно, какие именно правила существуют. Кроме того, союзы и структуры рассматриваются одинаково, но разумные правила для каждого должны быть разными.
суперкат
2
@supercat: если сформулировать правило для структур, фактический доступ всегда к примитивному типу. Тогда доступ через ссылку на примитивный тип является законным, потому что типы совпадают, и доступ через ссылку на содержащий тип структуры является законным, потому что это специально разрешено.
Бен Фойгт
2
@BenVoigt: Я не думаю, что общая начальная последовательность работает, если доступ не осуществляется через объединение. Смотрите goo.gl/HGOyoK, чтобы увидеть, что делает gcc. Если доступ к lvalue типа union через lvalue типа member (без использования оператора union-member-access) был законным, то wow(&u->s1,&u->s2)он должен был бы быть законным, даже если указатель используется для изменения u, и это отрицало бы большинство оптимизаций, которые Правило алиасинга было разработано для облегчения.
суперкат
82

Запись

Это выдержка из моего «Что такое строгое правило алиасинга и почему нас это волнует?» записать.

Что такое строгий псевдоним?

В C и C ++ псевдонимы связаны с тем, через какие типы выражений нам разрешен доступ к хранимым значениям. Как в C, так и в C ++ стандарт определяет, какие типы выражений допускаются для псевдонимов и каких типов. Компилятору и оптимизатору разрешается предполагать, что мы строго следуем правилам алиасинга, отсюда и термин строгое правило алиасинга . Если мы пытаемся получить доступ к значению с использованием недопустимого типа, оно классифицируется как неопределенное поведение ( UB ). Если у нас неопределенное поведение, все ставки отменены, результаты нашей программы перестают быть достоверными.

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

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

Предварительные примеры

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

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

У нас есть int *, указывающий на память, занятую int, и это допустимый псевдоним. Оптимизатор должен предполагать, что назначения через ip могут обновить значение, занимаемое x .

В следующем примере показан псевдоним, который приводит к неопределенному поведению ( пример в реальном времени ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

В функции foo мы берем int * и float * , в этом примере мы вызываем foo и устанавливаем оба параметра, чтобы они указывали на одну и ту же ячейку памяти, которая в этом примере содержит int . Обратите внимание, что reinterpret_cast говорит компилятору обрабатывать выражение так, как если бы оно имело тип, определенный его параметром шаблона. В этом случае мы говорим ему обрабатывать выражение & x, как если бы оно имело тип float * . Мы можем наивно ожидать, что результат второй cout будет равен 0, но при включенной оптимизации с использованием -O2 и gcc, и clang дают следующий результат:

0
1

Что может и не ожидаться, но совершенно правильно, так как мы вызвали неопределенное поведение. Число с плавающей запятой не может правильно называть объект int . Следовательно, оптимизатор может предположить, что константа 1, сохраненная при разыменовании i, будет возвращаемым значением, поскольку сохранение через f не может корректно влиять на объект int . Подсоединение кода в Compiler Explorer показывает, что это именно то, что происходит ( живой пример ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Оптимизатор, использующий анализ псевдонимов на основе типов (TBAA), предполагает, что 1 будет возвращен, и непосредственно перемещает постоянное значение в регистр eax, который несет возвращаемое значение. TBAA использует правила языков о том, какие типы разрешены для псевдонимов для оптимизации загрузки и хранения. В этом случае TBAA знает, что float не может использовать псевдонимы и int, и оптимизирует загрузку i .

Теперь к книге правил

Что именно стандарт говорит, что нам разрешено и не разрешено делать? Стандартный язык не является простым, поэтому для каждого элемента я постараюсь предоставить примеры кода, которые демонстрируют значение.

Что говорит стандарт C11?

Стандарт C11 говорит следующее в разделе 6.5 Выражения параграфа 7 :

Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов: 88) - тип, совместимый с действующим типом объекта,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- квалифицированная версия типа, совместимого с эффективным типом объекта,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

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

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang имеет расширение, а также позволяет присваивать int без знака int * значение int *, даже если они несовместимы.

- тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- агрегатный или объединенный тип, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- тип персонажа.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Что говорит C ++ 17 Draft Standard

Проект стандарта C ++ 17 в разделе 11 [basic.lval] гласит:

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

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - cv-квалифицированная версия динамического типа объекта,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - тип, подобный (как определено в 7.5) динамическому типу объекта,

(11.4) - тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - тип, который является типом со знаком или без знака, соответствующим cv-квалифицированной версии динамического типа объекта,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - агрегатный тип или тип объединения, который включает в себя один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая рекурсивно элемент или элемент нестатических данных субагрегата или автономного объединения),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - тип, который является (возможно, квалифицированным по cv) типом базового класса динамического типа объекта,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - тип char, unsigned char или std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Стоит отметить, что подписанный символ не включен в приведенный выше список, это заметное отличие от C, который говорит о типе символа .

Что такое Type Punning

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

Иногда мы хотим обойти систему типов и интерпретировать объект как другой тип. Это называется типом паннинга , чтобы переосмыслить сегмент памяти как другой тип. Тип punning полезен для задач, которые хотят получить доступ к базовому представлению объекта для просмотра, транспортировки или манипулирования. Типичные области, в которых мы находим использование типов ввода: компиляторы, сериализация, сетевой код и т. Д.

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

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Как мы видели ранее, это неверный псевдоним, поэтому мы вызываем неопределенное поведение. Но традиционно компиляторы не пользовались преимуществами строгих правил псевдонимов, и этот тип кода обычно просто работал, разработчики, к сожалению, привыкли делать такие вещи. Распространенный альтернативный метод для обозначения типов - через объединения, что допустимо в C, но неопределенное поведение в C ++ ( см. Живой пример ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

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

Как правильно печатать Pun?

Стандартный метод для определения типов в C и C ++ - это memcpy . Это может показаться немного сложным, но оптимизатор должен распознавать использование memcpy для обозначения типа, оптимизировать его и генерировать регистр для регистрации перемещения. Например, если мы знаем, что int64_t имеет тот же размер, что и double :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

мы можем использовать memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

При достаточном уровне оптимизации любой приличный современный компилятор генерирует код, идентичный ранее упомянутому методу reinterpret_cast или методу объединения для определения типов . Изучая сгенерированный код, мы видим, что он использует только регистр mov ( живой пример Compiler Explorer ).

C ++ 20 и bit_cast

В C ++ 20 мы можем получить bit_cast ( реализация доступна по ссылке в предложении ), который дает простой и безопасный способ ввода слов, а также может использоваться в контексте constexpr.

Ниже приведен пример того, как использовать bit_cast для ввода pun беззнакового целого типа с плавающей точкой ( смотрите в реальном времени ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

В случае, когда типы To и From не имеют одинаковый размер, это требует от нас использования промежуточной структуры15. Мы будем использовать структуру, содержащую символьный массив sizeof (unsigned int) ( предполагается, что 4-байтовое unsigned int ) будет типом From, а unsigned int - типом To . :

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

К сожалению, нам нужен этот промежуточный тип, но это текущее ограничение bit_cast .

Ловить строгие алиасинговые нарушения

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

gcc с использованием флагов -fstrict-aliasing и -Wstrict-aliasing может отлавливать некоторые случаи, хотя и без ложных срабатываний / отрицаний. Например, в следующих случаях в gcc будет сгенерировано предупреждение ( смотрите его вживую ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

хотя он не поймает этот дополнительный случай ( посмотри вживую ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Хотя clang разрешает эти флаги, он, по-видимому, фактически не реализует предупреждения.

Еще один инструмент, который у нас есть, - это ASan, который может улавливать смещенные грузы и запасы. Хотя это не является прямым строгим нарушением псевдонимов, это общий результат строгих нарушений псевдонимов. Например, в следующих случаях будут генерироваться ошибки времени выполнения при сборке с помощью clang с использованием -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Последний инструмент, который я порекомендую, специфичен для C ++ и не только инструмент, но и практика кодирования, не допускающая приведение в стиле C. И gcc, и clang будут производить диагностику для приведения в стиле C с использованием -Wold-style-cast . Это заставит любые неопределенные каламбуры типа использовать reinterpret_cast, в общем случае reinterpret_cast должен быть флагом для более тщательного анализа кода. Также проще выполнить поиск в базе кода для reinterpret_cast, чтобы выполнить аудит.

Для C у нас есть все инструменты, которые уже были рассмотрены, и у нас также есть TIS-интерпретатор, статический анализатор, который исчерпывающе анализирует программу для большого подмножества языка C. Учитывая C-версии предыдущего примера, где использование -fstrict-aliasing пропускает один случай ( смотрите его вживую )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter способен перехватить все три, в следующем примере tis-kernal вызывается как tis-интерпретатор (выходные данные редактируются для краткости):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

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

Шафик Ягмур
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Бхаргав Рао
3
Если бы я мог, +10, хорошо написал и объяснил, также с обеих сторон, писателей компиляторов и программистов ... единственная критика: было бы неплохо иметь контрпримеры выше, чтобы увидеть, что запрещено стандартом, это не очевидно вроде :-)
Габриэль
2
Очень хороший ответ Я только сожалею о том, что первоначальные примеры приведены на C ++, что затрудняет понимание таких людей, как я, которые знают или заботятся только о C и не имеют ни малейшего представления о том, что reinterpret_castможет сделать или что coutможет означать. (Можно упомянуть C ++, но первоначальный вопрос был о C и IIUC, эти примеры можно было бы так же правильно написать на C.)
Gro-
Что касается определения типа: так что если я записываю в файл массив некоторого типа X, а затем считываю из этого файла этот массив в память, указанную с помощью void *, то я приводю этот указатель к реальному типу данных, чтобы использовать его - это неопределенное поведение?
Михаил IV
44

Строгое псевдонимы относятся не только к указателям, но и к ссылкам, я написал статью об этом для вики для продвинутых разработчиков, и он был настолько хорошо принят, что я превратил его в страницу на своем консультационном веб-сайте. Это полностью объясняет, что это такое, почему это так сильно смущает людей и что с этим делать. Строгий Aliasing White Paper . В частности, это объясняет, почему объединения являются рискованным поведением для C ++, и почему использование memcpy является единственным переносимым исправлением как для C, так и для C ++. Надеюсь, это полезно.

phorgan1
источник
3
« Строгое псевдонимы относятся не только к указателям, но и к ссылкам ». На самом деле это относится к lvalues . « Использование memcpy - единственное исправление портативного » Слушай!
любопытный парень
5
Хорошая статья. Мое мнение: (1) эта проблема с псевдонимами - это чрезмерная реакция на плохое программирование - попытка защитить плохого программиста от его / ее вредных привычек. Если у программиста есть хорошие привычки, то этот псевдоним - просто неприятность, и проверки можно безопасно отключить. (2) Оптимизация на стороне компилятора должна проводиться только в известных случаях и в случае сомнений строго следовать исходному коду; принуждение программиста писать код для удовлетворения специфических особенностей компилятора, проще говоря, неправильно. Еще хуже сделать его частью стандарта.
Слэшмаис
4
@slashmais (1) "Чрезмерная реакция на плохое программирование " Чепуха. Это отказ от вредных привычек. Вы делаете это? Вы платите цену: нет гарантии для вас! (2) Хорошо известные случаи? Какие? Строгое правило псевдонимов должно быть «хорошо известно»!
любопытный парень
5
@curiousguy: После устранения некоторых недоразумений стало ясно, что язык C с правилами создания псевдонимов делает невозможным для программ реализацию пулов памяти, не зависящих от типа. Некоторые виды программ могут обойтись с помощью malloc / free, но другие нуждаются в логике управления памятью, лучше приспособленной к выполняемым задачам. Я удивляюсь, почему обоснование C89 использовало такой грубый пример причины правила наложения имен, поскольку их пример заставляет думать, что правило не будет создавать каких-либо серьезных трудностей при выполнении какой-либо разумной задачи.
Суперкат
5
@curiousguy, большинство комплектов компиляторов включают -fstrict-aliasing как значение по умолчанию для -O3, и этот скрытый контракт навязывается пользователям, которые никогда не слышали о TBAA и писали код, как, например, системный программист. Я не хочу показаться нечестным по отношению к системным программистам, но этот тип оптимизации следует оставить за пределами опции по умолчанию -O3 и должен быть оптимизацией для тех, кто знает, что такое TBAA. Не забавно смотреть на «ошибку» компилятора, которая оказывается кодом пользователя, нарушающим TBAA, особенно отслеживая нарушение уровня исходного кода в коде пользователя.
kchoi
34

Как дополнение к тому, что уже написал Дуг Т., вот простой тестовый пример, который, вероятно, запускает его с помощью gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Компилировать с gcc -O2 -o check check.c. Обычно (с большинством версий gcc, которые я пробовал) это выдает «проблему строгого алиасинга», потому что компилятор предполагает, что «h» не может быть тем же адресом, что и «k» в функции «check». Из-за этого компилятор оптимизирует if (*h == 5)компоновку и всегда вызывает printf.

Для тех, кого это интересует, код ассемблера x64, созданный gcc 4.6.3, работает на ubuntu 12.04.2 для x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Таким образом, условие if полностью ушло из ассемблерного кода.

Инго Блэкман
источник
если вы добавите второе короткое значение * j в check () и используете его (* j = 7), оптимизация исчезнет, ​​поскольку ggc не исчезнет, ​​если h и j не указывают на одно и то же значение. да оптимизация действительно умная.
Филипп Лхарди
2
Для того, чтобы сделать вещи больше удовольствия, использовать указатели на типы , которые не совместимы , но имеют одинаковый размер и представление (на некоторых системах это верно , например , long long*и int64_t*). Можно было бы ожидать, что здравомыслящий компилятор должен признать, что long long*и int64_t*может получить доступ к одному и тому же хранилищу, если они хранятся одинаково, но такое обращение уже не модно.
суперкат
Grr ... x64 - это соглашение Microsoft. Вместо этого используйте amd64 или x86_64.
SS Anne
Grr ... x64 - это соглашение Microsoft. Вместо этого используйте amd64 или x86_64.
SS Anne
17

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

Крис Шут-Янг
источник
1
См. Мой ответ здесь для соответствующих кавычек, особенно сносок, но написание шрифтов через союзы всегда было разрешено в C, хотя сначала это было плохо сформулировано. Вы хотите уточнить свой ответ.
Шафик Ягмур
@ShafikYaghmour: C89 явно позволял разработчикам выбирать случаи, в которых они могли бы или не могли бы с пользой распознать наказание за тип через профсоюзы. Реализация могла бы, например, указать, что для записи в один тип с последующим чтением другого, которое должно быть распознано как определение типа, если между записью и чтением программист выполнил одно из следующих действий : (1) вычислить значение lvalue, содержащее тип объединения [получение адреса члена будет соответствовать требованиям, если оно выполнено в правильной точке последовательности]; (2) преобразовать указатель на один тип в указатель на другой и получить доступ через этот ptr.
суперкат
@ShafikYaghmour: Реализация также может указывать, например, что пробивание типа между целочисленными значениями и значениями с плавающей запятой будет надежно работать только в том случае, если код выполняет fpsync()директиву между записью в виде fp и чтением в виде int или наоборот [в реализациях с отдельными целочисленными и конвейерами и кэшами FPU такая директива может быть дорогой, но не такой дорогой, как компилятор, выполняющий такую ​​синхронизацию при каждом доступе к объединению]. Или реализация может указывать, что полученное значение никогда не будет пригодным для использования, за исключением случаев, когда используются общие начальные последовательности.
суперкат
@ShafikYaghmour: Под C89, реализация может запретить большинство форм типа каламбуров, в то числе с помощью союзов, но эквивалентность между указателями профсоюзов и указателями на свои член подразумеваемых , что тип каламбурный была разрешено в реализациях , которые не прямо запрещают это.
суперкат
17

Согласно обоснованию C89, авторы стандарта не хотели требовать, чтобы компиляторы давали такой код:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

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

К сожалению, авторы C89 написали свое правило таким образом, что, если читать буквально, заставит даже следующую функцию вызывать Undefined Behavior:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

потому что он использует lvalue типа intдля доступа к объекту типа struct S, и intне входит в число типов, которые могут использоваться для доступа к struct S. Поскольку было бы абсурдно рассматривать любое использование элементов структур и объединений, не относящихся к символьному типу, как неопределенное поведение, почти каждый признает, что существуют, по крайней мере, некоторые обстоятельства, когда lvalue одного типа может использоваться для доступа к объекту другого типа. , К сожалению, Комитет по стандартам C не смог определить, каковы эти обстоятельства.

Большая часть проблемы связана с отчетом о дефектах № 028, в котором задан вопрос о поведении такой программы, как:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

В отчете о дефектах № 28 говорится, что программа вызывает неопределенное поведение, потому что действие записи члена объединения типа «double» и чтения одного типа «int» вызывает поведение, определяемое реализацией. Такие рассуждения бессмысленны, но формируют основу для правил эффективного типа, которые излишне усложняют язык, не делая ничего для решения исходной проблемы.

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

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Внутри нет конфликта, inc_intпотому что все обращения к хранилищу, через которое *pосуществляется доступ , выполняются с lvalue типа int, и здесь нет конфликта, testпотому что pон визуально получен из struct S, и при следующем sиспользовании все обращения к этому хранилищу, которые когда-либо будут сделаны через pуже произошло.

Если код был изменен немного ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Здесь существует конфликт псевдонимов между pи доступом к s.xотмеченной строке, потому что в этот момент выполнения существует другая ссылка, которая будет использоваться для доступа к тому же хранилищу .

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

Supercat
источник
Хорошо, было бы интересно прочитать предложение такого рода, которое было более или менее «тем, что мог бы сделать комитет по стандартам», которое достигло бы их целей, не внося такой большой сложности.
JRH
1
@jrh: я думаю, это было бы довольно просто. Признайте, что 1. Для создания псевдонимов во время конкретного выполнения функции или цикла необходимо использовать два разных указателя или значения во время этого выполнения для обращения к одному и тому же хранилищу в конфликтующем фашоне; 2. Признать, что в тех случаях, когда один указатель или lvalue только что визуально получен от другого, доступ ко второму - это доступ к первому; 3. Признать, что правило не предназначено для применения в случаях, которые на самом деле не включают псевдонимы.
суперкат
1
Точные обстоятельства, когда компилятор распознает недавно полученное значение lvalue, могут быть проблемой качества реализации, но любой удаленно приемлемый компилятор должен иметь возможность распознавать формы, которые gcc и clang намеренно игнорируют.
суперкат
11

Прочитав многие ответы, я чувствую необходимость что-то добавить:

Строгий псевдоним (который я опишу чуть позже) важен, потому что :

  1. Доступ к памяти может быть дорогим (с точки зрения производительности), поэтому данные обрабатываются в регистрах ЦП, прежде чем они записываются обратно в физическую память.

  2. Если данные в двух разных регистрах ЦП будут записаны в одно и то же пространство памяти, мы не можем предсказать, какие данные «выживут», когда мы кодируем в C.

    В сборке, где мы кодируем загрузку и выгрузку регистров ЦП вручную, мы узнаем, какие данные остаются нетронутыми. Но C (к счастью) абстрагируется от этой детали.

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

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

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

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

Если компилятор заметит, что два указателя указывают на разные типы (например, a int *и a float *), он будет считать, что адрес памяти отличается, и он не защитит от конфликтов адресов памяти, что приведет к более быстрому машинному коду.

Например :

Давайте возьмем следующую функцию:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Чтобы обработать случай, когда a == b(оба указателя указывают на одну и ту же память), нам нужно упорядочить и протестировать способ загрузки данных из памяти в регистры ЦП, чтобы код мог выглядеть примерно так:

  1. загрузить aи bиз памяти.

  2. добавить aк b.

  3. сохранить b и перезагрузить a .

    (сохранить из регистра ЦП в память и загрузить из памяти в регистр ЦП).

  4. добавить bк a.

  5. сохранить a(из регистра ЦП) в память.

Шаг 3 очень медленный, потому что ему нужен доступ к физической памяти. Тем не менее, это необходимо для защиты от случаев, когда aи bуказывают на тот же адрес памяти.

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

  1. Об этом можно сказать компилятору двумя способами, используя разные типы для указания. то есть:

    void merge_two_numbers(int *a, long *b) {...}
  2. Используя restrictключевое слово. то есть:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

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

Фактически, добавив restrictключевое слово, можно оптимизировать всю функцию:

  1. загрузить aи bиз памяти.

  2. добавить aк b.

  3. сохранить результат как до, так aи до b.

Эта оптимизация не могла быть сделана раньше из-за возможного столкновения (где aи bбыло бы утроено, а не удвоено).

Myst
источник
с ключевым словом restrict, на шаге 3, разве не следует сохранить результат только в 'b'? Звучит так, как будто результат суммирования будет также сохранен в «а». Нужно ли перезагрузить его снова?
NeilB
1
@NeilB - Да, ты прав. Мы только сохраняем b(не перезагружаем) и перезагружаем a. Надеюсь, теперь стало понятнее.
Myst
Возможно, псевдонимы на основе типов ранее предлагали некоторые преимущества restrict, но я думаю, что последний в большинстве случаев будет более эффективным, а ослабление некоторых ограничений registerпозволит ему заполнить некоторые случаи, где restrictэто не поможет. Я не уверен, что когда-либо было «важно» рассматривать Стандарт как полностью описывающий все случаи, когда программисты должны ожидать, что компиляторы будут распознавать свидетельства псевдонимов, а не просто описывать места, где компиляторы должны предполагать псевдонимы, даже когда нет конкретных доказательств его существования .
суперкат
Обратите внимание, что, хотя загрузка из основной ОЗУ очень медленная (и может зависать ядро ​​ЦП в течение длительного времени, если от результата зависят следующие операции), загрузка из кеша L1 довольно быстрая, и поэтому запись в строку кеша, которая недавно записывалась к тому же ядру. Таким образом, все, кроме первого чтения или записи по адресу, обычно будут достаточно быстрыми: разница между доступом к reg / mem addr меньше, чем разница между кэшированным / uncached mem addr.
любопытный парень
@curiousguy - хотя вы правы, «быстрый» в данном случае относителен. Кэш L1, вероятно, все еще на порядок медленнее, чем регистры процессора (я думаю, что в 10 раз медленнее). Кроме того, restrictключевое слово минимизирует не только скорость операций, но и их количество, что может быть значимым ... Я имею в виду, в конце концов, самая быстрая операция - вообще не операция :)
Myst
6

Строгий псевдоним не позволяет использовать разные типы указателей для одних и тех же данных.

Эта статья должна помочь вам понять проблему в деталях.

Джейсон Дагит
источник
4
Вы можете использовать псевдонимы между ссылками, а также между ссылкой и указателем. См. Мой учебник dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1
4
Разрешается иметь разные типы указателей на одни и те же данные. Строгий псевдоним возникает тогда, когда одна и та же ячейка памяти записывается через один тип указателя и читается через другой. Также допускаются некоторые различные типы (например, intструктура, которая содержит int).
ММ
-3

Технически в C ++ строгое правило псевдонимов, вероятно, никогда не применимо.

Обратите внимание на определение косвенности ( оператор * ):

Унарный оператор * выполняет косвенное обращение: выражение, к которому он применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, указывающее на объект или функцию, на которые указывает выражение .

Также из определения glvalue

Glvalue - это выражение, оценка которого определяет идентичность объекта, (... snip)

Таким образом, в любой четко определенной программной трассировке glvalue ссылается на объект. Так что так называемое правило строгого наложения не применяется никогда. Возможно, это не то, что хотели дизайнеры.

curiousguy
источник
4
Стандарт C использует термин «объект» для обозначения ряда различных понятий. Среди них последовательность байтов, которые выделены исключительно для какой-либо цели, необязательно-исключительная ссылка на последовательность байтов, в которую может быть записано или прочитано значение определенного типа , или такая ссылка, которая на самом деле имеет был или будет доступен в некотором контексте. Я не думаю, что есть какой-либо разумный способ определить термин «объект», который бы соответствовал всем способам, которыми его использует Стандарт.
суперкат
@supercat Неверно. Несмотря на ваше воображение, это на самом деле довольно последовательно. В ISO C это определяется как «область хранения данных в среде исполнения, содержимое которой может представлять значения». В ISO C ++ есть похожее определение. Ваш комментарий даже более неактуален, чем ответ, потому что все, что вы упомянули, - это способы представления для ссылки на содержимое объектов , в то время как ответ иллюстрирует концепцию C ++ (glvalue) своего рода выражений, тесно связанных с идентичностью объектов. И все правила псевдонимов в основном относятся к идентичности, но не к содержанию.
FrankHB
1
@FrankHB: Если кто-то заявляет int foo;, к чему обращается выражение lvalue *(char*)&foo? Это объект типа char? Этот объект появляется одновременно foo? Будет ли запись для fooизменения сохраненного значения вышеупомянутого объекта типа char? Если так, есть ли какое-либо правило, которое позволило charбы получить доступ к сохраненному значению объекта типа , используя lvalue типа int?
суперкат
@FrankHB: В отсутствие 6.5p7 можно просто сказать, что каждая область хранения одновременно содержит все объекты любого типа, которые могут поместиться в эту область хранения, и что доступ к этой области хранения одновременно обеспечивает доступ ко всем из них. Интерпретация таким способом использования термина «объект» в 6.5p7, однако, запретила бы делать что-либо со значениями, не относящимися к символьному типу, что явно было бы абсурдным результатом и полностью противоречило бы цели правила. Кроме того, понятие «объект», используемое везде, кроме 6.5p6, имеет статический тип времени компиляции, но ...
суперкат
1
sizeof (int) равно 4, создает ли объявление int i;четыре объекта каждого символьного типа in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` и i. Наконец, в Стандарте нет ничего, что позволяло бы даже volatileквалифицированному указателю получать доступ к аппаратным регистрам, которые не соответствуют определению «объекта».
суперкат