В моей компании используется сервер обмена сообщениями, который преобразует сообщение в 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
^ 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
const char*
is инициализируется, а затем преобразуется вMessageJ
. Этот вопрос спрашивает, могу ли я быть уверен, что инициализация всегда будет происходить первой? - person Jonathan Mee   schedule 23.03.2015bar
устанавливается (или инициализируется) из приведения значенияfoo
, тогда да, компилятор не будет переупорядочивать код таким образом, что foo может каким-то образом быть неинициализированным, когда происходит приведение для установкиbar
. Если я правильно понял ваш вопрос. - person jschultz410   schedule 23.03.2015char*
и обратно, но, кроме того, компилятор параноик в отношении доступа черезchar*
. То есть, он не будет свободно переупорядочивать операции через указатели разных типов, когда один из них являетсяchar*
. Я удивлен, что нет аналогичного утверждения оvoid*
, так что доступ через такие вещи, как memcpy и memset черезvoid*
, также обрабатывается параноидальным образом. - person jschultz410   schedule 23.03.2015auto bar = reinterpret_cast<const MessageX*>(temp);
, то это будет допустимо, поскольку будет соответствовать первому правилу, с добавлением const или без него. - person jschultz410   schedule 23.03.2015char
* к указателю любого типа, отличного отchar*
, и его разыменование обычно происходит в соответствии со строгим правилом псевдонимов.: cellperformance.beyond3d.com/articles/2006/06/ - person Jonathan Mee   schedule 23.03.2015auto bar = reinterpret_cast<const MessageJ*>(temp);
, является незаконным. Однако пример, который я привелauto bar = reinterpret_cast<const MessageX*>(temp);
, вполне законен. Обычно, если у вас есть указательchar*
(например, на случайный буфер и т. д.), то обычно недопустимо приводить этот указатель к указателю другого типа. Но, похоже, это не так в ваших примерах MessageServer. Там, кажется, был правильно сконструированный объект, адрес которого был приведен к char*, а затем обратно. - person jschultz410   schedule 23.03.2015PopMessage
.const
не имеет значения. Можете ли вы показать код для PopMessage? - person M.M   schedule 24.03.2015foo
на массив символов ИЛИ указывает ли он наMessageJ
, аfoo
является результатом приведения указателя наMessageJ
к char*. Насколько я могу судить, вы не отвечаете на это нигде в своем вопросе. - person MikeMB   schedule 24.03.2015struct Message
- этоenum
для его типа сообщения, поэтому мы будем преобразовывать отfoo
кMessageX
и определять, к какому типу сообщения должен быть приведенfoo
, здесьMessageJ
, затем мы будем приводитьfoo
к aconst MessageJ
и прочитайте оттуда. - person Jonathan Mee   schedule 24.03.2015foo
, а то, на что оно на самом деле указывает. Массив символов? Или фактический тип сообщения? Как инициализировалась область памяти? - person MikeMB   schedule 24.03.2015char*
кMessageX
, а скорее к типу перечисления, чтобы выяснить его базовый тип. - person jschultz410   schedule 24.03.2015MessageJ
.MessageX
используется для определения типа сообщенияfoo
. - person Jonathan Mee   schedule 24.03.2015enum
s. Так что да, я боюсь, что мы играем быстро и свободно. Или, скорее, я боюсь, что мы нарушили правило алиасинга, и оно вернется, чтобы укусить нас на определенной конфигурации. - person Jonathan Mee   schedule 24.03.2015char*
к этому базовому классу и безопасно получить значение перечисления. - person jschultz410   schedule 24.03.2015