Приведение указателя не дает lvalue. Зачем?
После публикации одного из моих самых противоречивых ответов здесь я осмелюсь задать несколько вопросов и в конечном итоге заполнить некоторые пробелы в моих знаниях.
Почему не выражение вида ((type_t *) x)
считается действительным lvalue, предполагая, что x
сам по себе указатель и lvalue, а не просто какое-то выражение?
Я знаю, что многие скажут, что "стандарт запрещает это", но с логической точки зрения это кажется разумным. По какой причине стандарт запрещает это? В конце концов, любые два указателя имеют одинаковый размер, а тип указателя - это просто абстракция времени компиляции, которая указывает соответствующее смещение, которое следует применять при выполнении арифметики указателя.
8 ответов
Еще лучший пример, одинарный +
дает значение, как и x+0
,
Основная причина в том, что все эти вещи, включая ваш состав, создают новую ценность. Приведение значения к типу, которым оно уже является, аналогичным образом создает новое значение, не говоря уже о том, имеют ли указатели на разные типы одинаковое представление или нет. В некоторых случаях новое значение оказывается равным старому значению, но в принципе это новое значение, оно не предназначено для использования в качестве ссылки на старый объект, и поэтому оно является rvalue.
Чтобы они были lvalue, стандарт должен был бы добавить некоторые особые случаи, когда определенные операции при использовании lvalue приводят к ссылке на старый объект, а не к новому значению. AFAIK нет особого спроса на эти особые случаи.
Результат приведения никогда не является самоцелью. Но *((type_t *) x)
это значение.
На самом деле вы правы и неправы одновременно.
В 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 */
Подвести итог:
- Если вы приведете указатель, это приведет к значению.
- С помощью
*
по указателю приводит к lvalue (*ptr
может быть назначен). ++
(как в*(arg)++
) нуждается в lvalue для работы (arg
должно быть lvalue)
следовательно ((int *)ptr)++
не удается, потому что ptr
это значение, но (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 *'
! С точки зрения стабильности важно не вводить такие контекстные значения для определенных типов.
Правильным для C является то, что существуют либо l-значения, либо их нет, и это не должно зависеть от некоторого произвольного контекста, который может быть удивительным.
Как отметил Йенс, уже есть один оператор для создания значений. Это оператор разыменования указателя, "унарный *
" (как в *ptr
).
Обратите внимание, что *ptr
можно записать как 0[ptr]
а также *ptr++
можно записать как 0[ptr++]
, Индексы массива являются lvalues, поэтому *ptr
это тоже значение.
Чего ждать? 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;
}
В С мы можем иметь и то и другое. Приведения, которые никогда не создают 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.
Ну, кастинг выполняет преобразование типов. В общем случае преобразование типов является нетривиальной операцией, которая полностью меняет представление значения. В этих обстоятельствах должно быть до боли очевидно, что результатом любого обращения не может быть lvalue.
Например, если у вас есть int i = 0;
переменная, вы можете преобразовать его в тип double
как (double) i
, Как вы можете ожидать, что результатом этого преобразования будет lvalue? Я имею в виду, это просто не имеет никакого смысла. Вы, очевидно, ожидаете, что сможете сделать (double) i = 3.0;
... Или же double *p = &(double) i;
Итак, что должно произойти со значением i
в первом примере, учитывая этот тип double
может даже не иметь такой же размер, как тип int
? И даже если бы они имели одинаковый размер, что бы вы ожидали?
Ваше предположение о том, что все указатели имеют одинаковый размер, неверно. В общем случае на языке Си (за редким исключением) разные типы указателей имеют разные размеры, разные внутренние представления и разные требования к выравниванию. Даже если они гарантированно имеют одинаковое представление, я все еще не понимаю, почему указатели должны быть отделены от всех других типов, и им следует уделить особое внимание в явных ситуациях приведения.
Наконец, вы, похоже, отстаиваете здесь то, что ваше исходное преобразование должно выполнить реинтерпретацию необработанной памяти одного типа указателя как другого типа указателя. Переосмысление необработанной памяти почти во всех случаях является взломом. Почему этот хак должен быть повышен до уровня языковой функции, мне не совсем понятно.
Поскольку это взлом, выполнение таких реинтерпретаций должно потребовать сознательных усилий со стороны пользователя. Если вы хотите выполнить это в своем примере, вы должны сделать *(type_t **) &x
который будет действительно переосмыслить x
как значение типа type_t *
, Но позволяя то же самое через простой (type_t *) x
будет катастрофой, совершенно не связанной с принципами проектирования языка Си.
С верхнего уровня это, как правило, бесполезно. Вместо '((type_t *) x) =' можно также пойти дальше и выполнить 'x =', предполагая, что x является указателем в вашем примере. Если кто-то хочет напрямую изменить значения, указанные адресом "x", но в то же время после интерпретации его как указателя на новый тип данных, тогда * ((type_t **) & x) = - это путь вперед. Опять же ((type_t **) & x) = не имеет смысла, не говоря уже о том, что это недопустимое значение lvalue.
Также в случаях ((int *)x)++, где, по крайней мере, "gcc" не жалуется по типу "lvalue", его можно интерпретировать как "x = (int *)x + 1".
Стандарт C был написан для поддержки экзотических машинных архитектур, которые требуют странных хаков для реализации модели указателя C для всех указанных типов. Чтобы позволить компилятору использовать наиболее эффективное представление указателя для каждого указываемого типа, стандарт C не требует, чтобы разные представления указателя были совместимыми. В такой экзотической архитектуре тип указателя void должен использовать самое общее и, следовательно, самое медленное из различных представлений. Есть несколько конкретных примеров таких устаревших архитектур в FAQ C: http://c-faq.com/null/machexamp.html
Обратите внимание, что если x
тип указателя, *(type_t **)&x
это значение. Однако доступ к нему как таковому, за исключением, возможно, в очень ограниченных обстоятельствах, вызывает неопределенное поведение из-за нарушений псевдонимов. Единственный раз, когда это может быть законно, это если типы указателей соответствуют типам указателей типа sign /unsigned или void/char, но даже в этом случае я сомневаюсь.
(type_t *)x
это не lvalue, потому что (T)x
никогда не бывает lvalue, независимо от типа T
или выражение x
, (type_t *)
это просто особый случай (T)
,