Ответ 1
Я рассмотрю ваш вопрос как два вопроса: 1) почему ->
даже существует, и 2) почему .
не автоматически разыгрывает указатель. Ответы на оба вопроса имеют исторические корни.
Почему существует ->
?
В одной из первых версий языка C (которую я буду называть CRM для Справочное руководство по C", который пришел с 6-м Edition Unix в мае 1975 года), оператор ->
имел очень исключительное значение, а не синоним комбинации *
и .
Язык C, описанный CRM, во многом отличался от современного C. В элементах CRM-структуры реализована глобальная концепция смещения байтов, которая может быть добавлена к любому значению адреса без ограничений типа. То есть все имена всех членов структуры имели независимый глобальный смысл (и, следовательно, должны были быть уникальными). Например, вы можете объявить
struct S {
int a;
int b;
};
и имя a
будет стоять за смещение 0, а имя b
будет стоять за смещение 2 (предполагая int
тип размера 2 и без заполнения). Язык требует, чтобы все члены всех структур в блоке перевода либо имели уникальные имена, либо стояли за одно и то же значение смещения. Например. в той же системе перевода вы можете дополнительно объявить
struct X {
int a;
int x;
};
и это будет нормально, так как имя a
будет постоянно стоять на смещение 0. Но это дополнительное объявление
struct Y {
int b;
int a;
};
будет формально недействительным, поскольку он попытался "переопределить" a
как смещение 2 и b
как смещение 0.
И здесь приходит оператор ->
. Поскольку каждое имя члена структуры имеет свой собственный самодостаточный глобальный смысл, выражения, поддерживаемые языком, такие как
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
Первое назначение интерпретировалось компилятором как "принимать адрес 5
, добавлять смещение 2
к нему и назначать 42
значению int
на результирующем адресе". То есть приведенное выше присваивало значение 42
int
по адресу 7
. Обратите внимание, что это использование ->
не заботилось о типе выражения в левой части. Левая сторона была интерпретирована как числовой адрес rvalue (будь то указатель или целое число).
Такая комбинация невозможна с комбинациями *
и .
. Вы не могли сделать
(*i).b = 42;
поскольку *i
уже является недопустимым выражением. Оператор *
, так как он отделен от .
, накладывает более строгие требования к типу на свой операнд. Чтобы обеспечить возможность обойти это ограничение, CRM представила оператор ->
, который не зависит от типа левого операнда.
Как отметил Кейт в комментариях, эта разница между комбинациями ->
и *
+ .
заключается в том, что CRM означает "расслабление требования" в 7.1.8: кроме ослабления требования что E1
имеет тип указателя, выражение E1−>MOS
в точности эквивалентно (*E1).MOS
Позже, в K & R C, многие функции, первоначально описанные в CRM, были значительно переработаны. Идея "член структуры как глобальный идентификатор смещения" была полностью удалена. И функциональность оператора ->
стала полностью идентичной функциональности комбинаций *
и .
.
Почему не удается .
автоматически разыменовать указатель?
Опять же, в версии CRM языка левый операнд оператора .
должен был быть lvalue. Это было единственным требованием, налагаемым на этот операнд (и тем, что отличает его от ->
, как объяснялось выше). Обратите внимание, что CRM не требует, чтобы левый операнд .
имел тип структуры. Это просто требовало, чтобы это была lvalue, любая lvalue. Это означает, что в CRM-версии C вы можете написать код, подобный этому
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
В этом случае компилятор записывал бы 55
в значение int
, расположенное в смещении байта 2 в блоке непрерывной памяти, известном как c
, хотя тип struct T
не имел поля с именем b
. Компилятор вообще не заботится о фактическом типе c
. Все, о чем он заботился, это то, что c
был lvalue: какой-то записываемый блок памяти.
Теперь обратите внимание, что если вы сделали это
S *s;
...
s.b = 42;
код будет считаться действительным (поскольку s
также является значением lvalue), и компилятор просто попытается записать данные в указатель s
сам по байту-смещению 2. Излишне говорить, что подобные вещи могли бы легко привести к переполнению памяти, но язык не касался таких вопросов.
т.е. в этой версии языка ваша предлагаемая идея о перегрузке оператора .
для типов указателей не будет работать: оператор .
уже имел очень специфическое значение при использовании с указателями (с указателями lvalue или с любыми значениями l вообще). Это была очень странная функциональность, без сомнения. Но он был там в то время.
Конечно, эта странная функциональность не является очень сильной причиной для введения перегруженного оператора .
для указателей (как вы сказали) в переработанной версии C-K & R C. Но это не было сделано. Возможно, в то время в CRM-версии C был написан код устаревшего кода, который должен был поддерживаться.
(URL-адрес справочного руководства 1975 года C может быть нестабильным. Другая копия, возможно с некоторыми незначительными отличиями, здесь.)