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

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

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

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

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

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

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

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

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

EDIT:

Ответ 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 делает его законным.

EDIT:

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

Это вопрос о строгом псевдониме.

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

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

Ответ 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.

Ответ 2

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

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

Если объект имеет тип T, и мы читаем/записываем его через X& и a 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).

Ответ 3

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

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* между ними, совершенно не имеет значения.

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

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

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

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

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

ИЗМЕНИТЬ:

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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