Можно ли избежать проблем с псевдонимами с помощью константных переменных

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

Я забеспокоился об этом после того, как задал этот вопрос. Я не знаю ни о каком плохом поведении на сервере обмена сообщениями. Возможно ли, что переменные const не вызывают проблем с псевдонимами?

Например, скажем, что foo определено в MessageServer одним из следующих способов:

  1. В качестве параметра: void MessageServer(const char* foo)
  2. Или как константная переменная вверху MessageServer: const char* foo = PopMessage();

Теперь MessageServer — это огромная функция, но она никогда ничего не присваивает foo, однако в 1 точке логики MessageServer foo будет приведена к выбранному типу сообщения.

auto bar = reinterpret_cast<const MessageJ*>(foo);

bar будет считываться только впоследствии, но будет широко использоваться для настройки объекта.

Возможна ли здесь проблема с псевдонимами, или меня спасает тот факт, что foo только инициализируется и никогда не изменяется?

ИЗМЕНИТЬ:

Ответ Jarod42 не находит проблем с приведением от const char* к MessageJ*, но я не уверен, что это имеет смысл.

Мы знаем, что это незаконно:

MessageX* foo = new MessageX;
const auto bar = reinterpret_cast<MessageJ*>(foo);

Мы говорим, что это как-то делает это законным?

MessageX* foo = new MessageX;
const auto temp = reinterpret_cast<char*>(foo);
auto bar = reinterpret_cast<const MessageJ*>(temp);

Мое понимание ответа Jarod42 заключается в том, что приведение к temp делает его законным.

ИЗМЕНИТЬ:

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

Это вопрос о строгом использовании псевдонимов. .

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

Я спрашиваю: Будет ли когда-нибудь оптимизирована инициализация объекта const путем приведения из char* ниже, когда этот объект приводится к другому типу объекта, например, я привожу к нему неинициализированные данные?


person Jonathan Mee    schedule 18.03.2015    source источник
comment
Вы имеете в виду выравнивание?   -  person TobiMcNamobi    schedule 18.03.2015
comment
@TobiMcNamobi Нет, я имел в виду псевдоним. Ответ Майка Сеймура на связанный вопрос довольно хорошо объясняет, как может возникнуть проблема с псевдонимами.   -  person Jonathan Mee    schedule 18.03.2015
comment
Я не специалист по языку C++, но могу сказать вам, что обычно в сетевом коде C и коде обмена сообщениями получение в выровненный буфер (например, возвращенный из malloc), а затем приведение указателя в этот буфер (например, char* или void*) в тип*, который, например, инкапсулирует заголовки сообщения. Вам все еще нужно обрабатывать порядок следования байтов и т. д., но это может сэкономить вам много кода де/сериализации.   -  person jschultz410    schedule 23.03.2015
comment
@ jschultz410 Да ... это сервер сообщений для нескольких классов одной и той же программы, не имеющий какое-либо отношение к выравниванию или сетевому взаимодействию.   -  person Jonathan Mee    schedule 23.03.2015
comment
Возможна ли здесь проблема с псевдонимами, или меня спасает тот факт, что foo только инициализируется и никогда не изменяется? Пока вы ничего не делаете (в том числе освобождаете/удаляете) с foo, кроме приведения его к нужному типу, проблем с псевдонимами быть не может. Этот момент мог бы быть еще яснее, если бы вместо этого foo был void*. Проблема псевдонимов возможна только в том случае, если вы пишете через foo или bar И читаете (или записываете) через другую переменную. Тогда компилятор сможет свободно переупорядочивать операции по своему усмотрению, и ваш код сможет делать разные вещи в разных компиляциях.   -  person jschultz410    schedule 23.03.2015
comment
@jschultz410 jschultz410 Это как раз то, о чем я спрашиваю, поскольку const char* is инициализируется, а затем преобразуется в MessageJ. Этот вопрос спрашивает, могу ли я быть уверен, что инициализация всегда будет происходить первой?   -  person Jonathan Mee    schedule 23.03.2015
comment
@JonathanMee Учитывая, что bar устанавливается (или инициализируется) из приведения значения foo, тогда да, компилятор не будет переупорядочивать код таким образом, что foo может каким-то образом быть неинициализированным, когда происходит приведение для установки bar. Если я правильно понял ваш вопрос.   -  person jschultz410    schedule 23.03.2015
comment
@JonathanMee Кроме того, я думаю, что ответ Jarod42 и страница cppreference.com говорят о том, что компилятор позволяет вам переинтерпретировать_приведение типа к char* и обратно, но, кроме того, компилятор параноик в отношении доступа через char*. То есть, он не будет свободно переупорядочивать операции через указатели разных типов, когда один из них является char*. Я удивлен, что нет аналогичного утверждения о void*, так что доступ через такие вещи, как memcpy и memset через void*, также обрабатывается параноидальным образом.   -  person jschultz410    schedule 23.03.2015
comment
@JonathanMee Насколько я понимаю ответ Jarod42, приведение к temp делает его законным. Установка температуры таким образом является законной. Установка бара таким образом незаконна, потому что это не соответствует ни одному из правил кастинга. Если вместо этого вы установите bar с помощью auto bar = reinterpret_cast<const MessageX*>(temp);, то это будет допустимо, поскольку будет соответствовать первому правилу, с добавлением const или без него.   -  person jschultz410    schedule 23.03.2015
comment
@ jschultz410 Хорошо, вот пример. Приведение char* к указателю любого типа, отличного от char*, и его разыменование обычно происходит в соответствии со строгим правилом псевдонимов.: cellperformance.beyond3d.com/articles/2006/06/   -  person Jonathan Mee    schedule 23.03.2015
comment
@JonathanMee Да, это часто так, поэтому пример, который вы привели выше auto bar = reinterpret_cast<const MessageJ*>(temp);, является незаконным. Однако пример, который я привел auto bar = reinterpret_cast<const MessageX*>(temp);, вполне законен. Обычно, если у вас есть указатель char* (например, на случайный буфер и т. д.), то обычно недопустимо приводить этот указатель к указателю другого типа. Но, похоже, это не так в ваших примерах MessageServer. Там, кажется, был правильно сконструированный объект, адрес которого был приведен к char*, а затем обратно.   -  person jschultz410    schedule 23.03.2015
comment
Разбрасывание указателей не является незаконным (если не нарушены никакие ограничения выравнивания); строгое правило псевдонимов срабатывает только тогда, когда вы читаете или записываете оба указателя   -  person M.M    schedule 23.03.2015
comment
@MattMcNabb Итак, это ответ на мой вопрос: я пишу по первому указателю (только инициализация) и впоследствии читаю из обоих указателей. Гарантированно ли, что инициализация произойдет первой?   -  person Jonathan Mee    schedule 23.03.2015
comment
@JonathanMee Операции с самими указателями (например, их инициализация) здесь не вопрос. Правило строгого псевдонима вступает в силу, только когда вы работаете с одной и той же базовой памятью через несколько ссылок (или указателей) несвязанных типов.   -  person jschultz410    schedule 23.03.2015
comment
Все зависит от того, что именно делает PopMessage. const не имеет значения. Можете ли вы показать код для PopMessage?   -  person M.M    schedule 24.03.2015
comment
Все это зависит от вопроса: указывает ли foo на массив символов ИЛИ указывает ли он на MessageJ, а foo является результатом приведения указателя на MessageJ к char*. Насколько я могу судить, вы не отвечаете на это нигде в своем вопросе.   -  person MikeMB    schedule 24.03.2015
comment
@MikeMB Первый член в любом struct Message - это enum для его типа сообщения, поэтому мы будем преобразовывать от foo к MessageX и определять, к какому типу сообщения должен быть приведен foo, здесь MessageJ, затем мы будем приводить foo к a const MessageJ и прочитайте оттуда.   -  person Jonathan Mee    schedule 24.03.2015
comment
Возможно, мой вопрос был не ясен: я имел в виду не то, к чему приводится foo, а то, на что оно на самом деле указывает. Массив символов? Или фактический тип сообщения? Как инициализировалась область памяти?   -  person MikeMB    schedule 24.03.2015
comment
@JonathanMee: Пока все, что вы делаете со ссылкой на MessageX, - это смотрите на значение перечисления, тогда это определенно хорошо определено для C из-за кругового обхода и того, что они являются совместимыми типами. Однако правила C++ более строгие, и здесь вы можете играть с ними слишком быстро и небрежно. Я думаю, что то, что вы делаете, может быть хорошо определено только в том случае, если MessageX является базовым классом для всех ваших типов сообщений, например. Или это может быть законным, если все ваши типы сообщений являются POD, и вы не приводите char* к MessageX, а скорее к типу перечисления, чтобы выяснить его базовый тип.   -  person jschultz410    schedule 24.03.2015
comment
@MikeMB Итак, изначально это был тип, к которому он был приведен, в данном случае MessageJ. MessageX используется для определения типа сообщения foo.   -  person Jonathan Mee    schedule 24.03.2015
comment
@ jschultz410 Хорошо, в настоящее время мы используем MessageX, который является POD. Но другие сообщения не гарантированно будут POD. И сообщение является на самом деле одним из других типов, несмотря на то, что их первые члены называются enums. Так что да, я боюсь, что мы играем быстро и свободно. Или, скорее, я боюсь, что мы нарушили правило алиасинга, и оно вернется, чтобы укусить нас на определенной конфигурации.   -  person Jonathan Mee    schedule 24.03.2015
comment
@JonathanMee: чтобы соответствовать стандарту, MessageX (или, может быть, Message) должен быть базовым классом для всех ваших различных классов Message*, и он должен содержать перечисление в качестве своего первого члена (действительно, это может быть все, что он содержит, с защищенными конструкторами и деструкторы). Затем ваша функция может привести char* к этому базовому классу и безопасно получить значение перечисления.   -  person jschultz410    schedule 24.03.2015
comment
@ jschultz410 jschultz410 Хотя это и не имеет прямого отношения к вопросу, это очень хорошее предложение.   -  person Jonathan Mee    schedule 24.03.2015
comment
@JonathanMee: На самом деле, позвольте мне взять назад то, что я сказал о том, что это полностью законно в C. Даже в C по стандарту вам, вероятно, понадобится тип объединения, назовите его Message, который содержит все ваши различные типы сообщений. Затем, поскольку все эти типы Message имеют перечисление в качестве первого члена, я считаю, что вы можете добавить перечисление в само объединение и исследовать его таким образом. По крайней мере, это было бы определенным поведением для приведения псевдонима и самого доступа. Тогда мы будем полагаться на тот факт, что преобразование между указателем на структуру и ее первым членом безопасно. Даже тогда я не уверен.   -  person jschultz410    schedule 24.03.2015


Ответы (5)


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

Пока вы имеете в виду, что он выполняет reinterpret_cast (или приведение в стиле C, которое переходит к reinterpret_cast):

MessageJ *j = new MessageJ();

MessageServer(reinterpret_cast<char*>(j)); 
// or PushMessage(reinterpret_cast<char*>(j));

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

MessageServer(char *foo)
{
  if (somehow figure out that foo is actually a MessageJ*)
  {
    MessageJ *bar = reinterpret_cast<MessageJ*>(foo);
    // operate on bar
  }      
}

// or

MessageServer()
{
  char *foo = PopMessage();

  if (somehow figure out that foo is actually a MessageJ*)
  {
    MessageJ *bar = reinterpret_cast<MessageJ*>(foo);
    // operate on bar
  }      
}

Обратите внимание, что я специально исключил константы из ваших примеров, так как их наличие или отсутствие не имеет значения. Вышеприведенное допустимо, когда базовый объект, на который указывает foo, фактически является MessageJ, в противном случае это неопределенное поведение. Повторное приведение к char* и обратно дает исходный типизированный указатель. Действительно, вы можете переинтерпретировать_приведение к указателю любого типа и обратно и получить исходный типизированный указатель. Из этой ссылки:

Только следующие преобразования могут быть выполнены с помощью reinterpret_cast...

6) Выражение lvalue типа T1 может быть преобразовано в ссылку на другой тип T2. Результатом является значение lvalue или значение x, ссылающееся на тот же объект, что и исходное значение lvalue, но другого типа. Временные объекты не создаются, копии не создаются, конструкторы или функции преобразования не вызываются. Полученная ссылка может быть безопасно доступна только в том случае, если это разрешено правилами псевдонимов типов (см. ниже)...

Псевдоним типа

Когда указатель или ссылка на объект типа T1 переопределяется (или приводится в стиле C) к указателю или ссылке на объект другого типа T2, приведение всегда завершается успешно, но результирующий указатель или ссылка могут быть доступны только в том случае, если оба T1 и T2 являются стандартными типами компоновки, и выполняется одно из следующих условий:

  • T2 – динамический тип объекта (возможно, с указанием cv) ...

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

Таким образом, преобразование ваших указателей туда и обратно допустимо, но как насчет потенциальных проблем с псевдонимами?

Возможна ли здесь проблема с псевдонимами, или меня спасает тот факт, что foo только инициализируется и никогда не изменяется?

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

#include <iostream>

int foo(int *x, long *y)  
{
  // foo can assume that x and y do not alias the same memory because they have unrelated types
  // so it is free to reorder the operations on *x and *y as it sees fit
  // and it need not worry that modifying one could affect the other
  *x = -1;
  *y =  0;
  return *x;
}

int main()
{
  long a;
  int  b = foo(reinterpret_cast<int*>(&a), &a);  // violates strict aliasing rule

  // the above call has UB because it both writes and reads a through an unrelated pointer type
  // on return b might be either 0 or -1; a could similarly be arbitrary
  // technically, the program could do anything because it's UB

  std::cout << b << ' ' << a << std::endl;

  return 0;
}

В этом примере, благодаря строгому правилу псевдонимов, компилятор может предположить в foo, что настройка *y не может повлиять на значение *x. Таким образом, он может решить, например, просто вернуть -1 как константу. Без строгого правила алиасинга компилятору пришлось бы предположить, что изменение *y может фактически изменить значение *x. Следовательно, ему придется применить заданный порядок операций и перезагрузить *x после установки *y. В этом примере может показаться достаточно разумным навязать такую ​​паранойю, но в менее тривиальном коде это сильно ограничит переупорядочивание и удаление операций и заставит компилятор гораздо чаще перезагружать значения.

Вот результаты на моей машине, когда я скомпилирую вышеуказанную программу по-другому (Apple LLVM v6.0 для x86_64-apple-darwin14.1.0):

$ g++ -Wall test58.cc
$ ./a.out
0 0
$ g++ -Wall -O3 test58.cc
$ ./a.out
-1 0

В вашем первом примере foo — это const char *, а bar — это const MessageJ * реинтерпретация_преобразования из foo. Далее вы указываете, что базовым типом объекта на самом деле является MessageJ и что чтение через const char * не выполняется. Вместо этого он приводится только к const MessageJ *, из которого затем выполняются только чтения. Поскольку вы не читаете и не пишете через псевдоним const char *, то не может быть проблем с оптимизацией псевдонимов при доступе через ваш второй псевдоним. Это связано с тем, что с базовой памятью не выполняются потенциально конфликтующие операции через ваши псевдонимы несвязанных типов. Однако, даже если вы прочитаете foo, потенциальной проблемы все равно не может быть, поскольку такой доступ разрешен правилами псевдонимов типов (см. пишет происходящее здесь.

Давайте теперь удалим квалификаторы const из вашего примера и предположим, что MessageServer действительно выполняет некоторые операции записи в bar и, кроме того, что функция по какой-то причине также считывает foo (например, - печатает шестнадцатеричный дамп памяти). Обычно здесь может быть проблема с псевдонимами, поскольку чтение и запись происходят через два указателя на одну и ту же память через несвязанные типы. Однако в этом конкретном примере нас спасает тот факт, что foo — это char*, который получает специальную обработку компилятором:

Псевдоним типа

Когда указатель или ссылка на объект типа T1 переопределяется (или приводится в стиле C) к указателю или ссылке на объект другого типа T2, приведение всегда завершается успешно, но результирующий указатель или ссылка могут быть доступны только в том случае, если оба T1 и T2 являются типами стандартной компоновки, и верно одно из следующих утверждений: ...

  • T2 – символ или символ без знака

Оптимизация строгого псевдонима, которая разрешена для операций через ссылки (или указатели) несвязанных типов, специально запрещена, когда используется ссылка (или указатель) char. Вместо этого компилятор должен быть параноиком, что операции через ссылку char (или указатель) могут влиять на операции, выполняемые через другие ссылки (или указатели). В модифицированном примере, где операции чтения и записи выполняются как для foo, так и для bar, вы по-прежнему можете иметь определенное поведение, поскольку foo является char*. Таким образом, компилятору не разрешается оптимизировать для изменения порядка или устранения операций над вашими двумя псевдонимами таким образом, чтобы это противоречило последовательному выполнению написанного кода. Точно так же он вынужден параноидально относиться к перезагрузке значений, которые могли быть затронуты операциями через любой из псевдонимов.

Ответ на ваш вопрос заключается в том, что до тех пор, пока ваши функции являются правильными указателями на тип через char* обратно к его исходному типу, ваша функция безопасна, даже если вы будете чередовать чтение (и, возможно, запись, см. предостережение в конце EDIT) через псевдоним char* с чтением и записью через псевдоним базового типа.

Эти два технические справочники (3.10.10) полезны для ответа на ваш вопрос. Эти другие ссылки помогают лучше понять техническую информацию.

====
РЕДАКТИРОВАТЬ: В комментариях ниже zmb возражает, что, хотя char* может законно псевдоним другого типа, обратное неверно, поскольку несколько источников, кажется, говорят в разных формах: что исключение char* из строгого правила псевдонимов является асимметричным, «односторонним» правилом.

Давайте изменим приведенный выше пример кода со строгим псевдонимом и спросим, ​​приведет ли эта новая версия к неопределенному поведению?

#include <iostream>

char foo(char *x, long *y)
{
  // can foo assume that x and y cannot alias the same memory?
  *x = -1;
  *y =  0;
  return *x;
}

int main()
{
  long a;
  char b = foo(reinterpret_cast<char*>(&a), &a);  // explicitly allowed!

  // if this is defined behavior then what must the values of b and a be?

  std::cout << (int) b << ' ' << a << std::endl;

  return 0;
}

Я утверждаю, что это определенное поведение и что и a, и b должны быть равны нулю после вызова foo. Из стандарта C++ (3.10.10):

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

  • динамический тип объекта...

  • тип char или unsigned char...

^ 52: Цель этого списка - указать те обстоятельства, при которых объект может или не может быть псевдонимом.

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

Теперь у компилятора нет общего способа всегда статически знать в foo, что указатель x на самом деле является псевдонимом y или нет (например, представьте, что foo был определен в библиотеке). Возможно, программа могла бы обнаруживать такие псевдонимы во время выполнения, изучая значения самих указателей или консультируясь с RTTI, но накладные расходы, которые это повлечет, не стоили бы того. Вместо этого, лучший способ вообще скомпилировать foo и разрешить определенное поведение, когда x и y действительно встречаются с псевдонимами друг друга, — это всегда предполагать, что они могут (т. Е. Отключить строгую оптимизацию псевдонимов, когда char* находится в игре).

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

$ g++ -Wall test59.cc
$ ./a.out
0 0
$ g++ -O3 -Wall test59.cc
$ ./a.out
0 0

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

Давайте рассмотрим некоторые из на первый взгляд конфликтующий источники:

Обратное неверно. Приведение char* к указателю любого типа, отличного от char*, и разыменование его обычно происходит в соответствии со строгим правилом псевдонимов. Другими словами, приведение указателя одного типа к указателю несвязанного типа через char* не определено.

Выделенный жирным шрифтом бит объясняет, почему эта цитата не относится ни к проблеме, рассматриваемой в моем ответе, ни к примеру, который я только что привел. Как в моем ответе, так и в примере доступ к памяти с псевдонимом осуществляется как через char*, так и через фактический тип самого объекта, поведение которого можно определить.

И C, и C++ позволяют получить доступ к любому типу объекта через char * (или, в частности, через lvalue типа char). Они не позволяют получить доступ к объекту char через произвольный тип. Так что да, правило — это правило «в один конец».

Опять же, жирным шрифтом объясняется, почему это утверждение не относится к моим ответам. В этом и подобных контрпримерах доступ к массиву символов осуществляется через указатель несвязанного типа. Даже в C это UB, потому что массив символов может быть не выровнен, например, в соответствии с требованиями типа с псевдонимом. В C++ это UB, потому что такой доступ не соответствует ни одному из правил псевдонимов типов, поскольку базовый тип объекта на самом деле char.

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

int   value;  
int  *p = &value;  
char *q = reinterpret_cast<char*>(&value);

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

В стандарте и во многих примерах четко указано, что «запись через q, затем чтение через p (или значение)» может быть четко определенным поведением. Что не так совершенно ясно, но то, за что я здесь ратую, так это то, что «записать через p (или значение), затем прочитать через q» всегда хорошо определено. Более того, я утверждаю, что «чтение и запись через p (или значение) могут произвольно чередоваться с чтением и записью в q» с четко определенным поведением.

Теперь есть одно предостережение к предыдущему утверждению, и почему я продолжал разбрасывать слово «может» по всему тексту выше. Если у вас есть ссылка типа T и ссылка char, которые ссылаются на одну и ту же память, то произвольное чередование операций чтения и записи по ссылке T с чтением по ссылке char всегда правильно определено. Например, вы можете сделать это, чтобы повторно распечатать шестнадцатеричный дамп базовой памяти, когда вы изменяете его несколько раз по ссылке T. Стандарт гарантирует, что к этим чередующимся доступам не будут применяться строгие оптимизации псевдонимов, которые в противном случае могут привести к неопределенному поведению.

Но как насчет записи через псевдоним ссылки char? Ну, такие записи могут быть или не быть четко определенными. Если запись по ссылке char нарушает инвариант базового типа T, вы можете получить неопределенное поведение. Если такая запись неправильно изменила значение указателя члена T, вы можете получить неопределенное поведение. Если такая запись изменила значение члена T на значение ловушки, то вы можете получить неопределенное поведение. И так далее. Однако в других случаях запись через ссылку char может быть полностью четко определена. Например, изменение порядка следования байтов uint32_t или uint64_t путем чтения и записи в них через ссылку char с псевдонимом всегда хорошо определено. Таким образом, полностью ли такие записи определены или нет, зависит от особенностей самих операций записи. Несмотря на это, стандарт гарантирует, что его строгая оптимизация псевдонимов не изменит порядок или не устранит такие записи относительно. другие операции с псевдонимом памяти, которые сами по себе могут привести к неопределенному поведению.

person jschultz410    schedule 23.03.2015
comment
Я бы не стал доверять второй статье, на которую вы ссылаетесь. См., например, этот вопрос: stackoverflow.com/ вопросы/24869510/ - person zmb; 23.03.2015
comment
@zmb: эта проблема была исправлена ​​в статье. Он хотел использовать >>= и <<=, а не >> и <<. Это по-прежнему не имеет ничего общего с алиасингом, а скорее является UB, потому что оно дважды записывает одно и то же значение без промежуточной точки последовательности. - person jschultz410; 23.03.2015
comment
Верно. В любом случае, я думаю, что у вас все равно будет определенное поведение, потому что один из двух ваших псевдонимов — char* — неверен. char* может давать псевдонимы другим типам, но другие типы по-прежнему не могут давать псевдонимы char*. Обратите внимание, как написано [если] T2 не является char*. Не сказано, если T1 или T2 не является char*. - person zmb; 23.03.2015
comment
@zmb: я не знаю. Такое асимметричное обращение кажется мне довольно странным. Если char* может псевдоним любого типа, то для меня имеет смысл быть параноиком с обеих сторон всякий раз, когда char* находится в игре. Это, безусловно, правда, что если мы приводим указатель к типу к char*, оперируем этим char* в базовой памяти, а затем оглядываемся назад на значение нашего исходного типа, что это будет затронуто. Вы утверждаете, что если мы затем изменим память через наш первоначально введенный псевдоним и снова проверим память через char*, то у нас будет UB... - person jschultz410; 24.03.2015
comment
Я с тобой согласен. Мне тоже это кажется странным, но я думаю, что так оно и есть. См. здесь: cellperformance.beyond3d.com/articles/2006. /06/. В частности, обратное неверно. Приведение char* к указателю любого типа, отличного от char*, и разыменование его обычно является нарушением строгого правила алиасинга. - person zmb; 24.03.2015
comment
Или, например, см. принятый ответ здесь: stackoverflow.com/questions/28239793/ - person zmb; 24.03.2015
comment
@zmb: Я определенно понимаю путаницу, так как сомневаюсь в собственном понимании. Однако пример в вашей первой ссылке преобразует указатель в несовместимый тип указателя через char*, а затем работает с памятью через этот несовместимый тип указателя. Так что это не относится напрямую к моему приведенному выше примеру. Пример в вашей следующей ссылке работает с массивом символов через несовместимый uint32_t*, что может легко привести к ошибке шины из-за проблем с выравниванием. Таким образом, это тоже не относится к этому примеру. - person jschultz410; 24.03.2015
comment
@zmb: В моем приведенном выше примере я считаю, что это правило T2 является (возможно, квалифицированным cv) динамическим типом объекта и гарантирует, что как char* псевдоним ссылки на исходный тип, так и ссылка исходного типа будет псевдонимом char*. - person jschultz410; 24.03.2015
comment
Давайте продолжим обсуждение в чате. - person zmb; 24.03.2015
comment
Во-первых, не может быть проблемы с псевдонимом, потому что ваш код выполняет чтение только из памяти с псевдонимом, что безопасно. - Хм? Строгое правило псевдонимов применяется как к чтению, так и к записи. - person M.M; 24.03.2015
comment
@MattMcNabb Если записи не выполняются, компилятор может свободно переупорядочивать операции чтения по своему усмотрению, не влияя на какие-либо результаты. Строгие правила алиасинга могут кусаться только тогда, когда в игру вступает хотя бы одна запись, и операции больше нельзя свободно переупорядочивать или оптимизировать. - person jschultz410; 24.03.2015
comment
@jschultz в этом случае поведение undefined, так что да, компиляция может делать то, что хочет. - person M.M; 24.03.2015
comment
@MattMcNabb: Не могли бы вы более подробно объяснить, что вы имеете в виду и почему это UB в соответствии со стандартом? - person jschultz410; 24.03.2015
comment
В вашем первом примере *x = -1; вызывает неопределенное поведение (long имеет псевдоним int). Вы отмечаете комментарий на строке после этого, но к тому времени уже слишком поздно, UB уже сработал. Чтение *x также будет UB (даже если a было инициализировано). В вашем последнем примере все в порядке, и было бы также хорошо, если бы foo написал *x и *y. Строгие правила алиасинга одинаково относятся к чтению и записи. - person M.M; 24.03.2015
comment
@MattMcNabb: Вы поставили мне минус, потому что в моем примере, демонстрирующем UB, я, возможно, разместил комментарий не в той строке? - person jschultz410; 24.03.2015
comment
Нет, из-за First не может быть проблемы с псевдонимом, потому что ваш код выполняет чтение только в памяти с псевдонимом, . Это неправильно. Остальная часть этого параграфа также неверна. Доступ char может быть правильным, но доступ через другой тип может быть неправильным. Это как если бы вы мысленно применяли строгое правило алиасинга между двумя типами в списке параметров функции. - person M.M; 24.03.2015
comment
@MattMcNabb: Тогда я спрошу еще раз в связи с конкретным вопросом ОП: не могли бы вы более подробно объяснить, что вы имеете в виду и почему это UB в соответствии со стандартом? - person jschultz410; 24.03.2015
comment
извините, вы спрашиваете, каков ответ на вопрос ОП? - person M.M; 24.03.2015
comment
@MattMcNabb: Хорошо, думаю, я понимаю, что вы получаете. Весь мой ответ основан на идее о том, что адрес правильно сконструированного типа приводится к char*, а затем внутри функции, возвращаемой туда и обратно, обратно к ее истинному базовому типу. Если это не так, то все дело в FUBAR'е. - person jschultz410; 24.03.2015
comment
Да, все в порядке, если PopMessage создает (или ссылается) на подлинный MessageJ (или аналогичный, такой как const MessageJ), и все FUBAR, если это не так. - person M.M; 24.03.2015
comment
Я опубликовал свое собственное мнение сейчас. Первоначально я ждал, пока ОП разъяснит, что делает PopMessage, что имеет решающее значение. - person M.M; 24.03.2015
comment
@MattMcNabb: я обновил свой ответ, чтобы прояснить, что весь мой ответ зависит от предположения, что он правильно переключает указатели на объекты через char *. Кроме того, обновил его, чтобы указать, что доступ через несвязанный ссылочный тип сам по себе является UB, как вы указали. - person jschultz410; 24.03.2015

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

Псевдоним относится к процессу чтения или записи объекта через glvalue другого типа, чем объект.

Если объект имеет тип T, и мы читаем/записываем его через X& и Y&, то вопросы таковы:

  • Может X псевдоним T?
  • Может Y псевдоним T?

Напрямую не имеет значения, может ли X использовать псевдоним Y или наоборот, как вы, кажется, сосредоточились в своем вопросе. Но если X и Y полностью несовместимы, компилятор может сделать вывод, что не существует такого типа T, который может быть совмещен как с X, так и с Y, поэтому он может предположить, что две ссылки относятся к разным объектам.

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

const char *PopMessage()
{
     static MessageJ foo = .....;
     return reinterpret_cast<const char *>(&foo);
}

то правильно написать:

const char *ptr = PopMessage();
auto bar = reinterpret_cast<const MessageJ*>(foo);

auto baz = *bar;    // OK, accessing a `MessageJ` via glvalue of type `MessageJ`
auto ch = ptr[4];   // OK, accessing a `MessageJ` via glvalue of type `char`

и так далее. const не имеет к этому никакого отношения. На самом деле, если бы вы не использовали здесь const (или отбросили его), то вы могли бы без проблем писать через bar и ptr.

С другой стороны, если бы PopMessage выглядело примерно так:

const char *PopMessage()
{
    static char buf[200];
    return buf;
}

тогда строка auto baz = *bar; вызовет UB, потому что char не может быть псевдонимом MessageJ. Обратите внимание, что вы можете использовать новое размещение для изменения динамического типа объекта (в этом случае говорят, что char buf[200] перестало существовать, а новый объект, созданный новым размещением, существует и имеет тип T).

person M.M    schedule 24.03.2015
comment
Вот кое-что, чего я не видел ни в одном из своих чтений: как насчет void* псевдонимов? Я понимаю, что у вас не может быть ссылки на lvalue типа void, но как насчет чего-то вроде: void foo(void *p1, size_t p1size, int *p2) { *p2 = 5; memset(p1, 0, p1size); } int main() { int i; foo(&i, sizeof(i), &i); /* what's the value of i now? */ }? char* безопаснее, чем void* в данном случае??? - person jschultz410; 24.03.2015
comment
@ jschultz410 jschultz410 Это не нарушает строгое использование псевдонимов, потому что вы не пишете через значение gl и т. Д. И т. Д. Memset указан как модифицирующий память, он не зависит от какой-либо конкретной реализации. - person M.M; 24.03.2015
comment
Верно, но наверняка составители компиляторов должны быть так же параноидальны в отношении void*, как и в отношении char*? В моем примере они должны предположить, что p1 и p2 могут быть псевдонимами друг друга, и поэтому они не могут свободно переупорядочивать или исключать такие операции. В приведенном выше коде, если foo вернул *p2, он должен вернуть 0. И в main i тоже должен быть равен нулю. - person jschultz410; 24.03.2015
comment
Компилятор может предположить, что вызовы стандартных функций делают то, что стандарт говорит об этой функции. Нельзя предположить, что memset не повлияет на *p2. (Но могло бы, если бы вы использовали restrict, я думаю) - person M.M; 24.03.2015
comment
Согласованный. Хорошая мысль о restrict! Им следует поторопиться и добавить это в стандарт C++. - person jschultz410; 24.03.2015

Я так понимаю, что вы делаете что-то вроде этого:

enum MType { J,K };
struct MessageX { MType type; };

struct MessageJ {
    MType type{ J };
    int id{ 5 };
    //some other members
};
const char* popMessage() {
    return reinterpret_cast<char*>(new MessageJ());
}
void MessageServer(const char* foo) {
    const MessageX* msgx = reinterpret_cast<const MessageX*>(foo);
    switch (msgx->type) {
        case J: {
            const MessageJ* msgJ = reinterpret_cast<const MessageJ*>(foo);
            std::cout << msgJ->id << std::endl;
        }
    }
}

int main() {
    const char* foo = popMessage();
    MessageServer(foo);
}

Если это правильно, то выражение msgJ->id допустимо (как и любой доступ к foo), так как msgJ имеет правильный динамический тип. msgx->type, с другой стороны, подвергается UB, потому что msgx имеет несвязанный тип. Тот факт, что указатель на MessageJ был приведен к const char* между ними, совершенно не имеет значения.

Как упоминалось другими, вот соответствующая часть стандарта (значение gl является результатом разыменования указателя):

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

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

Что касается обсуждения приведения к char* и приведения из char*:
Возможно, вы знаете, что в стандарте не говорится о строгом алиасинге как таковом, он предоставляет только приведенный выше список. Строгий псевдоним — это один из методов анализа, основанный на этом списке, чтобы компиляторы могли определить, какие указатели потенциально могут создавать псевдонимы друг друга. С точки зрения оптимизации не имеет значения, был ли указатель на объект MessageJ приведен к char* или наоборот. Компилятор не может (без дальнейшего анализа) предположить, что char* и MessageX* указывают на разные объекты, и не будет выполнять никаких оптимизаций (например, переупорядочение) на основе этого.

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

РЕДАКТИРОВАТЬ:

Я спрашиваю: будет ли инициализация const-объекта путем приведения из char* когда-либо оптимизирована ниже, когда этот объект приводится к другому типу объекта, так что я привожу из неинициализированных данных?

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

person MikeMB    schedule 24.03.2015
comment
Ре. последний абзац: если выравнивание правильное, то это не UB из-за выравнивания (но это UB из-за алиасинга) - person M.M; 24.03.2015
comment
@Matt McNabb: Это может показаться странным, но я бы сказал, что это UB, потому что так сказано в Стандарте. Проблемы с выравниванием или оптимизация на основе анализа алиасинга могут быть причинами, по которым комитет решил указать это таким образом и почему UB может проявиться в неработающей программе. - person MikeMB; 25.03.2015

При использовании типа (const)char* проблемы с псевдонимом нет, см. последний пункт:

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

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) динамическому типу объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим cv-квалифицированной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая, рекурсивно, элемент или нестатический элемент данных подагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • тип char или unsigned char.
person Jarod42    schedule 18.03.2015
comment
Но подождите, я получаю доступ через bar, который не относится к типу char. Вы говорите, что это нормально, так как я инициализировал bar из типа char? - person Jonathan Mee; 18.03.2015
comment
char* может иметь псевдоним T*, так что преобразование в порядке. - person Jarod42; 18.03.2015
comment
Итак, эта страница говорит, что я могу преобразовать в char*, но не от char*. - person Jonathan Mee; 18.03.2015
comment
Последний пункт не применяется. Этот код обращается (или, предположительно, будет обращаться) к объекту char через файл MessageJ. Это отличается от доступа к MessageJ через char. Однако вы правы в том, что это зависит от того, что PopMessage() делает внутри. Например, если у PopMessage было MessageJ *, которое было приведено к char *, то для другого кода совершенно нормально вернуть его обратно и использовать. - person M.M; 23.03.2015

Другой ответ достаточно хорошо ответил на вопрос (это прямая цитата из стандарта С++ в https://isocpp.org/files/papers/N3690.pdf стр. 75), поэтому я просто укажу на другие проблемы в том, что вы делаете.

Обратите внимание, что ваш код может столкнуться с проблемами выравнивания. Например, если выравнивание MessageJ равно 4 или 8 байтам (обычно для 32-разрядных и 64-разрядных машин), строго говоря, обращение к произвольному указателю массива символов в качестве указателя MessageJ является неопределенным поведением.

Вы не столкнетесь с какими-либо проблемами на архитектурах x86/AMD64, поскольку они допускают невыровненный доступ. Однако когда-нибудь вы можете обнаружить, что код, который вы разрабатываете, портирован на мобильную архитектуру ARM, и тогда невыровненный доступ будет проблемой.

Поэтому кажется, что вы делаете то, чего не должны делать. Я бы подумал об использовании сериализации вместо доступа к массиву символов в качестве типа MessageJ. Единственная проблема заключается не в потенциальных проблемах с выравниванием, а в том, что данные могут иметь разное представление в 32-битной и 64-битной архитектурах.

person juhist    schedule 20.03.2015