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

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

11 ответов


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

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

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 пункт 71 - это неопределенное поведение. К сожалению, вы все еще можете кодировать таким образом,может быть получите некоторые предупреждения, скомпилируйте его нормально, только чтобы иметь странные неожиданное поведение при запуске кода.

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

чтобы понять, почему это поведение не определено, мы должны подумать о том, что строгое правило псевдонима покупает компилятор. В принципе, с этим правилом ему не нужно думать о вставке инструкций для обновления содержимого buff каждый запуск цикла. Вместо этого, при оптимизации, с некоторыми досадно необоснованными предположениями о сглаживании он может опустить эти инструкции, load buff[0] и buff[1] в CPU регистрируется один раз перед запуском цикла и ускоряет тело цикла. Прежде чем была введена строгая ступенчатость, компилятор должен был жить в состоянии паранойи, что содержание 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, и он может или не может решить загрузить или не загружать buff снова. Если SendMessage is часть другого API, который компилируется отдельно, вероятно, имеет инструкции по загрузке содержимого buff. Опять же, возможно, вы находитесь в C++, и это какая-то шаблонная реализация заголовка, которую компилятор считает встроенной. Или, может быть, это просто что-то, что ты написал в своем .файл C для вашего удобства. В любом случае, неопределенное поведение все еще может последовать. Даже когда мы знаем кое-что из того, что происходит под капотом, это все равно нарушение правила, поэтому никакое четко определенное поведение не гарантировано. Так просто при обертывании функции, которая принимает наш буфер с разделителями word, не обязательно помогает.

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

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

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

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

Новичок остерегайтесь

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

сноска

1 типы, которые C 2011 6.5 7 позволяет lvalue для доступа являются:

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

лучшее объяснение, которое я нашел, - это Майк Эктон,Понимание Строгого Сглаживания. Он немного ориентирован на разработку PS3, но это в основном просто GCC.

из статьи:

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

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

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


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

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

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

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

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

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

два изменения были небольшими:glvalue вместо lvalue, и разъяснение дела aggregate / union.

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


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

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

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

73) или 88) цель этого списка-указать те обстоятельства, при которых объект может быть или не может быть изменен.


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


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

проверка.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    , (%rdi)
movq    , (%rsi)
movl    $.LC0, %edi
jmp puts

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


Примечание

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

что такое строгое сглаживание?

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

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

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

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

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

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?
}

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

0
1

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

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 (live Compiler Explorer пример).

C++20 и bit_cast

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

пример использования bit_cast ввести каламбур a unsigned int to плавание, (посмотреть его в прямом эфире):

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

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

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 и -устриц.-ступенчатость может поймать некоторые случаи, хотя и не без ложных срабатываний / негативов. Например, следующие случаи будут генерировать предупреждение в 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 с помощью -fsanitize=адреса

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-интерпретатор, статический анализатор который исчерпывающе анализирует программу для a большие подмножества языка Си. Учитывая 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-interpreter (вывод редактируется для краткости):

./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, который в настоящее время находится в развитии. Это дезинфицирующее добавляет проверку типа информации сегмент теневой памяти и проверяет доступ, чтобы увидеть, если они нарушают правила псевдонимов. Инструмент потенциально должен быть в состоянии поймать все нарушения сглаживания, но может иметь большие накладные расходы во время выполнения.


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


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

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

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

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

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

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

большая часть проблемы является результатом дефекта отчета #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 из-за перекрытия между созданием и использованием двух указатели, которые сделали бы вещи намного более ясными без добавления "эффективных типов" или другой такой сложности.


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

строгое сглаживание (которое я опишу немного)это важно, потому что:

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

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

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

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

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

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

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

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

:

предположим следующую функцию:

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

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

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

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

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

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

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

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

Шаг 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 to b.

  3. сохранить как a и b.

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


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

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


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

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

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

и с определение glvalue

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

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