Ответ 1
Моя компания использует сервер обмена сообщениями, который получает сообщение в const char *, а затем передает его в тип сообщения.
Пока вы подразумеваете, что он выполняет reinterpret_cast (или приведение в стиле C, которое переходит к reinterpret_cast):
MessageJ *j = new MessageJ();
MessageServer(reinterpret_cast<char*>(j));
// or PushMessage(reinterpret_cast<char*>(j));
а затем принимает этот тот же указатель и reinterpret_cast его обратно к фактическому базовому типу, то этот процесс является полностью законным:
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
}
}
Обратите внимание, что я специально удалил const из ваших примеров, поскольку их присутствие или отсутствие не имеет значения. Вышеприведенное является законным, если базовый объект, который foo
указывает на на самом деле, a MessageJ
, в противном случае это поведение undefined. Параметр reinterpret_cast'ing до char*
и обратно возвращает исходный типизированный указатель. Действительно, вы можете reinterpret_cast указатель типа any и обратно и получить исходный типизированный указатель. Из эта ссылка:
С reinterpret_cast можно выполнить только следующие преобразования:
6) Выражение lvalue типа T1 может быть преобразовано в ссылку на другой тип T2. Результатом является значение lvalue или xvalue, относящееся к тому же объекту, что и исходное lvalue, но с другим типом. Временное создание не производится, копирование не производится, не создаются конструкторы или функции преобразования. Результирующую ссылку можно получить только безопасно, если это разрешено правилами псевдонимов типа (см. Ниже)...
Наложение типов
Когда указатель или ссылка на объект типа T1 представляет собой reinterpret_cast (или приведение в стиле C) к указателю или ссылке на объект другого типа T2, приведение всегда выполняется успешно, но результирующий указатель или ссылка могут быть доступны только если оба T1 и T2 являются стандартными типами макета, и одно из следующего верно:
- T2 - это (возможно, cv-квалифицированный) динамический тип объекта...
Эффективно, reinterpret_cast'ing между указателями разных типов просто инструктирует компилятор переинтерпретировать указатель как указывающий на другой тип. Что еще более важно для вашего примера, однако, повторное отключение назад к оригинальному типу, а затем работающее на нем безопасно. Это потому, что все, что вы сделали, инструктировало компилятор переинтерпретировать указатель как указывающий на другой тип, а затем снова попросил компилятор переосмыслить тот же самый указатель, что и указатель на исходный тип.
Итак, конвертация ваших указателей в оба конца является законной, но как насчет потенциальных проблем с псевдонимом?
Возможна ли проблема с псевдонимом, или же факт, что 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 UB
std::cout << b << ' ' << a << std::endl;
return 0;
}
В этом примере, благодаря правилу строгого aliasing, компилятор может принять в 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 *
reinterpret_cast'ed от foo
. Вы также указываете, что тип, лежащий в основе объекта, фактически является MessageJ
и что никакие чтения не выполняются через const char *
. Вместо этого он отсылается только к const MessageJ *
, из которого затем выводятся только чтения. Поскольку вы не читаете и не пишете псевдоним const char *
, тогда проблема с оптимизацией псевдонимов не может быть вызвана вашими доступами через ваш второй псевдоним. Это связано с тем, что не существует потенциально конфликтных операций, выполняемых в базовой памяти через ваши псевдонимы несвязанных типов. Однако даже если вы прочитали foo
, тогда потенциальная проблема все еще не может быть проблемой, так как такие права доступа разрешены правилами псевдонимов типа (см. Ниже), и любое упорядочение чтения через foo
или bar
приведет к те же результаты, потому что здесь нет записей.
Давайте теперь отбросим спецификаторы const из вашего примера и предположим, что MessageServer
выполняет некоторые операции записи на bar
и, кроме того, по какой-либо причине функция также читает через foo
(например, - печатает шестнадцатеричный дамп Память). Обычно здесь может возникать проблема сглаживания, поскольку мы читаем и записываем, проходя через два указателя на одну и ту же память через несвязанные типы. Однако в этом конкретном примере мы сохраняем тот факт, что foo
является char*
, который получает специальную обработку компилятором:
Наложение типов
Когда указатель или ссылка на объект типа T1 представляет собой reinterpret_cast (или приведение в стиле C) к указателю или ссылке на объект другого типа T2, приведение всегда выполняется успешно, но результирующий указатель или ссылка могут быть доступны только если оба T1 и T2 являются стандартными типами макета, и одно из следующего верно:...
- T2 char или без знака char
Оптимизации с строгим сглаживанием, разрешенные для операций с помощью ссылок (или указателей) несвязанных типов, категорически запрещены, когда в режиме воспроизведения находится ссылка (или указатель) char
. Компилятор вместо этого должен быть параноидальным, что операции с помощью ссылки char
(или указателя) могут влиять и быть затронуты операциями, выполняемыми с помощью других ссылок (или указателей). В модифицированном примере, где чтение и запись работают как на foo
, так и на bar
, вы все равно можете определить поведение, потому что foo
- это char*
. Поэтому компилятору не разрешается оптимизировать, чтобы переупорядочивать или устранять операции над двумя вашими псевдонимами способами, конфликтующими с последовательным выполнением кода, как написано. Точно так же он вынужден параноидально перегружать значения, которые могут быть затронуты операциями через любой псевдоним.
Ответ на ваш вопрос заключается в том, что до тех пор, пока ваши функции будут правильно округлять указатели на переход к типу с помощью char*
обратно к исходному типу, ваша функция будет безопасной, даже если вы должны чередовать чтение (и потенциально пишет, см. оговорку в конце EDIT) с помощью псевдонима char*
с чтениями + записи через псевдоним основного типа.
Эти технические ссылки (3.10.10) полезны для ответа на ваш вопрос. Эти другие ссылки помогают лучше понять техническую информацию.
====
EDIT. В комментариях ниже объекты zmb, которые в то время как char*
могут иметь законный псевдоним другого типа, что обратное неверно, поскольку несколько источников, похоже, говорят в разных формах: исключение char*
к правилу строгого сглаживания - это асимметричное правило "одностороннее".
Изменим мой пример кода с строгим сглаживанием и спросим, аналогично ли эта новая версия приведет к поведению undefined?
#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;
}
Я утверждаю, что это определено поведение и что а и b должны быть равны нулю после вызова foo
. Из С++ standard (3.10.10):
Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue, отличного от одного из следующих типов, поведение undefined: ^ 52
динамический тип объекта...
a char или неподписанный char тип...
^ 52: Цель этого списка - указать те обстоятельства, при которых объект может или не может быть сглажен.
В приведенной выше программе я обращаюсь к сохраненному значению объекта как с его фактическим типом, так и с типом char, поэтому он определяется поведением, и результаты должны соответствовать серийному исполнению кода, как написано.
Теперь нет общего способа, чтобы компилятор всегда статически знал в foo
, что указатель x
фактически псевдонимы y
или нет (например, представьте, если foo
был определен в библиотеке). Возможно, программа может обнаружить такое наложение на время выполнения, исследуя значения самих указателей или консультируясь с RTTI, но накладные расходы, которые это понесло бы, не стоили бы того. Вместо этого лучший способ общей компиляции foo
и допускать определенное поведение, когда x
и y
происходят с псевдонимом друг друга, всегда должны предполагать, что они могут (т.е. - отключить строгие оптимизации псевдонимов, когда a char*
в игре).
Вот что происходит, когда я компилирую и запускаю указанную выше программу:
$ g++ -Wall test59.cc
$ ./a.out
0 0
$ g++ -O3 -Wall test59.cc
$ ./a.out
0 0
Этот результат не согласуется с предыдущей аналогичной программой строгого сглаживания. Это не является диспозитивным доказательством того, что я прав насчет стандарта, но разные результаты одного и того же компилятора дают достойные доказательства того, что я могу быть прав (или, по крайней мере, один важный компилятор, похоже, понимает стандарт таким же образом).
Давайте рассмотрим некоторые казалось бы, конфликтующие источники:
Обратное неверно. Приведение a char * к указателю любого типа, отличного от char *, и разыменование происходит, как правило, в правиле строгого правила псевдонимов. Другими словами, отбрасывание указателя одного типа на указатель несвязанного типа через char * составляет undefined.
Полужирный бит - это то, почему эта цитата не применяется к проблеме, рассмотренной моим ответом, или к примеру, который я только что дал. Как в моем ответе, так и в примере, доступ к памяти с псевдонимом осуществляется как через char*
, так и фактический тип самого объекта, который может быть определен как поведение.
Оба C и С++ разрешают доступ к любому типу объекта с помощью char * (или, в частности, lvalue типа char). Они не позволяют получить доступ к a char объекту через произвольный тип. Итак, да, правило является правилом "одного пути".
Опять же, полужирный бит - это то, почему это утверждение не относится к моим ответам. В этом и подобных встречных примерах массив символов обращается через указатель несвязанного типа. Даже в C это UB, потому что, например, массив символов не может быть выровнен в соответствии с требованиями типа aliased. В С++ это UB, потому что такой доступ не соответствует ни одному из правил псевдонимов типа, поскольку базовым типом объекта является char
.
В моих примерах мы сначала имеем действительный указатель на правильно сконструированный тип, который затем сглаживается с помощью char*
, а затем считывает и записывает эти два псевдонимов указателя чередуются, что может быть определено поведением. Таким образом, кажется, что существует некоторая путаница и слияние между строгим исключением aliasing для 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
. Стандарт гарантирует, что строгие оптимизации псевдонимов не будут применяться к этим чередованным доступам, что в противном случае может привести к поведению undefined.
Но как насчет записи через ссылочный псевдоним char
? Ну, такие записи могут быть или не быть четко определены. Если запись через ссылку char
нарушает инвариант базового типа T
, вы можете получить поведение undefined. Если такая запись неправильно изменяет значение указателя элемента T
, вы можете получить поведение undefined. Если такая запись изменила значение элемента T
на значение ловушки, вы можете получить поведение undefined. И так далее. Однако в других случаях записи с помощью ссылки char
могут быть полностью определены. Например, переупорядочивание континентности uint32_t
или uint64_t
путем чтения + записи с помощью ссылочной ссылки char
является всегда. Таким образом, независимо от того, являются ли такие записи полностью определенными или нет, зависит от особенностей самих записей. Несмотря на это, стандарт гарантирует, что его строгая оптимизация псевдонимов не будет изменять порядок или исключать такие записи w.r.t. другие операции над псевдонимом памяти таким образом, который сам по себе может привести к поведению undefined.