Приведение указателя не дает lvalue. Зачем? - PullRequest
11 голосов
/ 16 сентября 2011

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

Почему выражение типа ((type_t *) x) не считается допустимым lvalue, предполагая, что x сам по себе является указателем и lvalue, а не просто каким-либо выражением?

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

Ответы [ 8 ]

8 голосов
/ 16 сентября 2011

Результат приведения никогда сам по себе не является lvalue. Но *((type_t *) x) - это lvalue.

7 голосов
/ 16 сентября 2011

Еще лучший пример: унарный + дает значение r, как и x+0.

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

Чтобы они были lvalues, стандарт должен был бы добавить некоторые особые случаи, когда определенные операции при использовании lvalue приводят к ссылке на старый объект, а не к новому значению.AFAIK, нет особого спроса на эти особые случаи.

3 голосов
/ 16 сентября 2011

Поскольку выражения приведения в общем случае не дают lvalues.

2 голосов
/ 16 сентября 2011

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

Например, если у вас есть переменная int i = 0;, вы можете преобразовать ее в тип double как (double) i. Как вы можете ожидать, что результатом этого преобразования будет lvalue? Я имею в виду, это просто не имеет никакого смысла. Вы, очевидно, ожидаете, что сможете сделать (double) i = 3.0; ... или double *p = &(double) i; Итак, что должно произойти со значением i в предыдущем примере, учитывая, что тип double может даже не иметь тот же размер, что и тип int? И даже если бы они имели одинаковый размер, что бы вы ожидали?

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

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

Поскольку это взлом, выполнение таких реинтерпретаций должно потребовать сознательных усилий со стороны пользователя. Если вы хотите выполнить это в своем примере, вы должны выполнить *(type_t **) &x, который действительно будет интерпретировать x как lvalue типа type_t *. Но допустить то же самое с помощью простого (type_t *) x было бы катастрофой, совершенно не связанной с принципами проектирования языка C.

1 голос
/ 30 декабря 2016

На самом деле вы правы и неправы одновременно.

В C есть возможность безопасно типизировать любое lvalue к любому lvalue.Однако синтаксис немного отличается от вашего прямого подхода:

lvalue-указатели могут быть преобразованы в lvalue-указатели другого типа , например, в C:

char *ptr;

ptr = malloc(20);
assert(ptr);
*(*(int **)&ptr)++ = 5;

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

char *ptr;

ptr = malloc(20);
assert(ptr);
*ptr++ = 0;
*(*(int **)&ptr)++ = 5;  /* can throw an exception due to misalignment */

Подводя итог:

  • Если вы приведете указатель, этоприводит к r-значению.
  • Использование * для указателя приводит к l-значению (*ptr может быть назначено).
  • ++ (как в *(arg)++) требуетсяlvalue для работы (arg должно быть lvalue)

Следовательно, ((int *)ptr)++ терпит неудачу, потому что ptr является lvalue, а (int *)ptr - нет.++ можно переписать как ((int *)ptr += 1, ptr-1), и это (int *)ptr += 1, который завершается неудачно из-за приведения, приводящего к чистому значению.


Обратите внимание, что это не является языковым недостатком.Литье не должно производить lvalues.Посмотрите на следующее:

(double *)1   = 0;
(double)ptr   = 0;
(double)1     = 0;
(double *)ptr = 0;

Первые 3 не компилируются.Зачем кому-то ожидать, что 4-я строка будет скомпилирована?Языки программирования никогда не должны демонстрировать такое удивительное поведение.Более того, это может привести к неясному поведению программ.Подумайте:

#ifndef DATA
#define DATA double
#endif
#define DATA_CAST(X) ((DATA)(X))

DATA_CAST(ptr) = 3;

Это не скомпилируется, верно?Однако, если ваши ожидания оправдались, это неожиданно компилируется с cc -DDATA='double *'!С точки зрения стабильности важно не вводить такие контекстные l-значения для определенных приведений.

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


Как отметил Дженс , уже есть один оператор для создания lvalue.Это оператор разыменования указателя, «унарный *» (как в *ptr).

Обратите внимание, что *ptr можно записать как 0[ptr], а *ptr++ можно записать как 0[ptr++],Индексы массива являются lvalue, поэтому *ptr также является lvalue.

Подождите, что?0[ptr] должно быть ошибкой, верно?

На самом деле, нет.Попытайся!Это допустимо C. Следующая C-программа действительна на 32/64-битной Intel во всех отношениях, поэтому она компилируется и успешно работает:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int
main()
{
  char *ptr;

  ptr = malloc(20);
  assert(ptr);

  0[(*(int **)&ptr)++] = 5;
  assert(ptr[-1]==0 && ptr[-2]==0 && ptr[-3]==0 && ptr[-4]==5);

  return 0;
}

В C мы можем иметь и то, и другое.Приведения, которые никогда не создают lvalues.И возможность использовать приведение таким образом, чтобы мы могли поддерживать свойство lvalue в живых.

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

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

Следовательно, вместо неправильного *((int *)ptr)++ мы можем написать *(*(int **)&ptr)++.Это также гарантирует, что ptr в этом выражении уже должно быть lvalue.Или написать это с помощью препроцессора C:

#define LVALUE_CAST(TYPE,PTR) (*((TYPE *)&(PTR)))

Так что для любого переданного в void *ptr (который может маскироваться под char *ptr), мы можем написать:

*LVALUE_CAST(int *,ptr)++ = 5;

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

1 голос
/ 17 сентября 2011

На верхнем уровне это, как правило, бесполезно. Вместо '((type_t *) x) =' можно также пойти дальше и выполнить 'x =', предполагая, что x является указателем в вашем примере. Если кто-то хочет напрямую изменить значения, указанные адресом «x», но в то же время после интерпретации его как указателя на новый тип данных, тогда * ((type_t **) & x) = - это путь вперед. Опять же ((type_t **) & x) = не имеет смысла, не говоря уже о том, что это недопустимое lvalue.

Также в случаях ((int *) x) ++, где, по крайней мере, «gcc» не жалуется на «lvalue», его можно интерпретировать как «x = (int *) x + 1»

1 голос
/ 16 сентября 2011

Обратите внимание, что если x является типом указателя, *(type_t **)&x является lvalue.Однако доступ к нему как таковому, за исключением, возможно, в очень ограниченных обстоятельствах, вызывает неопределенное поведение из-за нарушений псевдонимов.Единственный раз, когда может быть законным, это если типы указателей соответствуют типам указателей типа sign / unsigned или void / char, но даже тогда я сомневаюсь.lvalue, потому что (T)x никогда не является lvalue, независимо от типа T или выражения x.(type_t *) - это особый случай (T).

1 голос
/ 16 сентября 2011

Стандарт C был написан для поддержки экзотических машинных архитектур, которые требуют странных хаков для реализации модели указателя C для всех указанных типов.Чтобы позволить компилятору использовать наиболее эффективное представление указателя для каждого указываемого типа, стандарт C не требует, чтобы разные представления указателя были совместимы.В такой экзотической архитектуре тип указателя void должен использовать самое общее и, следовательно, самое медленное из различных представлений.В FAQ по C есть несколько конкретных примеров таких устаревших архитектур: http://c -faq.com / null / machexamp.html

...