Pull to refresh

Comments 32

А зачем вообще для типов и для переменных использовать разные символы, в Паскале и там и там используется "^" и всё работает, да и в B, вроде бы тоже, достаточно было только звёздочки (по мне, так достаточно одного амперсанда, раз уж звёздочка это умножение для переменных). Так что такое глубокое умозаключение про амперсанд, при введении двух знаков одновременно, мне вообще не понятно, ибо для меня вопрос «нафига» остаётся не раскрытым.
Ничего не перепутали?

В Паскале используется "&" для оператора взятия адреса, "^" для определения ссылочного типа, "^" для взятия значения по ссылке.

В С используется "&" для оператора взятия адреса, "*" для определения ссылочного типа, "*" для взятия значения по ссылке.
В Паскале используется "&" для оператора взятия адреса,

Вообще-то '@'.

Да, верно, ошибся с копипастом :-)

Но вопрос тот-же про «и там и там используется ^», что имеется ввиду?
Ошибка. Символ каретки в Паскале так же используется, как и звездочка в сях, в зависимости от позиции — перед переменной/перед типом для разыменовывания или задания типа. В Паскале каретка до — тип, после — разыменовывание:
PMyType = ^TMyType - указатель на TMyType
var
abc: PMyType;
...
:= abc^.field;
Ах, это Addr() ("@" я не использую), я просто не понял о чём речь, мне подумалось, что там разные знаки: для указания, что это ссылочный тип и для работы с переменными ссылочного типа.
В Си, кстати, звездочка намного логичнее — объявление вида «вот такое получится, если разыменовать», а в Паскале — нет, мнемоники никакой нет.
Паскале — нет, мнемоники никакой нет.

Почему нет? Вроде, есть. Не везде допустима, но есть.

var
abc: ^Integer;
...
abc^ := 123;
Вы не поняли.
В сях будет так:
int *a
Т.е. «если дереференснуть а, будет int», потому что * перед — это разыменовывание. Так что такое объявление логично.
А паскалевское такой мнемоники не имеет — каретка прыгает то перед переменной, то после.
Если было бы ^abc = было бы так же мнемоничненько.
Для меня то, что в паскале — логичнее. Есть переменная, есть ее тип.
Тип с крышкой (^) впереди — это ссылочный тип. Все просто и понятно.
Вы все равно не поняли.
В сях есть мнемоника, которая не требует запоминания.
Тип с крышкой (^) впереди — это ссылочный тип. Все просто и понятно

Нет, каретка ставится перед тем, на что ссылку описываем, а не перед самим типом. Никакой логикой тут не пахнет.
PMyType = ^TMyType;
Ссылочный тип тут PMyType, а не TMyType. Было бы PMyType = TMyType^; было бы как в Си, логично. Но низя: требование однопроходности паскалевского компилятора. Не скажу, что я не доволен этим балансом, tradeoffом так сказать.
В сях есть мнемоника, которая не требует запоминания.

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

Независимо от того, где ставится крышка/звездочка это требует запоминания. Или только слева, или только справа, или одно слева, другое справа.

Ссылочный тип тут PMyType, а не TMyType.

Ссылочный тип здесь ^TMyType и, по счастливой случайности, PMyType.
Ну как не требует запоминания? Программисты на C святым духом что ли пользуются?

Независимо от того, где ставится крышка/звездочка это требует запоминания. Или только слева, или только справа, или одно слева, другое справа.

Вы все равно не поняли, или я выразился неточно. В С запоминается только одно: звездочка и потом переменная — это разыменовывание. Всё. В Паскале запоминается как синтаксис разыменовавания, так и синтаксис объявления, потому что он нелогичен. В С синтаксис объявления органично следует из синтаксиса разыменовывания.
Эта логика начинает резко хромать, когда определения усложняются:

typedef int (*fun)(void*);


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

А вот такое компиляторы уже не осиливают, выдавая ошибки:

typedef int (*fun1)(int) (*fun2)(int);


хотя, казалось бы, чего сложно — определить fun2 как указатель на функцию с int параметром и указателем на функцию int->int в результате…
ан нет, случился зашкал сложности, и без промежуточных typedefʼов не выкрутиться.

Стиль таких определений в Pascal, Go (*) и многих других — читаемее и не страдает такими ограничениями. Цена же за это — что надо, например, явно писать слова var — для переменных, func — для определений функций. Как по мне, цена вполне разумная, польза перевешивает.

(*) Хоть я его и громко ругаю за кучу прочего.
А вот такое компиляторы уже не осиливают, выдавая ошибки:

typedef int (*fun1)(int) (*fun2)(int);

Компилятор как раз осилит, а вот мы, люди, такое можем в голове и не удержать. :)

… определить fun2 как указатель на функцию с int параметром и указателем на функцию int->int в результате

typedef int (*(*fun1)(int))(int);

Вот здесь дан очень хороший ответ:
stackoverflow.com/questions/10758811/c-syntax-for-functions-returning-function-pointers

От себя добавлю, что очень помогает typeof, если он определён компилятором:

typedef typeof(typeof(int(*)(int)) (*)(int)) fn2;

// Если нет аллергии на макросы:
#define FN(PARAMS, RET) typeof(RET(*) PARAMS)
typedef FN((int), FN((int), int)) fn3;
> typedef int (*(*fun1)(int))(int);

Вы определили fun1, а не fun2. Похоже, это опечатка — если написать fun2, то получится то, что ожидается (?)

Вот об этом я и говорю — если в спокойной обстановке тут путаешься в таких определениях, то что же будет в нормальном рабочем цейтноте?

А ещё — сколько времени прошло, прежде чем хоть кто-то заметил, где я ошибся?

> Вот здесь дан очень хороший ответ:

Угу, с тремя слоями вкручивания в определение…

> typedef typeof(typeof(int(*)(int)) (*)(int)) fn2;

и ещё одна путаница, и уже другого стиля (!) Итого — два разных стиля.

Спасибо за исправление моего примера. И всё это считаю безусловными примерами в пользу того, что синтаксис определений в том порядке, как Pascal/Ada/Go/etc., работает на пользу написания/чтения/сопровождения, а синтаксис C — против.
Спорить с тем, что в C не самый удачный синтаксис для указателей на функции и тем, что рядом с ними, никто не будет.

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

Вы определили fun1, а не fun2. Похоже, это опечатка — если написать fun2, то получится то, что ожидается (?)

Да, получится то, что вы описывали.

Это, кстати, не самый удивляющий пример.
Без typedef определить функцию, возвращающую функцию, которая возвращает и принимает функцию — вот тут в скобочках и звёздочках нормальные люди начнут «плавать».
и ещё одна путаница, и уже другого стиля (!) Итого — два разных стиля.

Это способ описания указателей на функции без надобности запоминать странные правила их определения.
Не уверен, что понял, о какой «путаннице» вы говорили.

С этим макросом стиль определения будет чем-то похож на тот, что в Go:

FN((int), int)
=
func(int) int

FN(FN((int, string), float), FN(int) int)
=
func(func(int, string) float) func(int) int


Если я допущу здесь какую-то опечатку или пропущу запятую — не вижу в этом катастрофы. Описана идея, а не исполняемые сниппеты.
дизайнеры

Designer = разработчик, или в данном контексте «создатель» (языка программирования). Дизайнер — тот, кто создает одежду или интерьер для квартиры, такова коннотация в русском яыке.
И чем людям адресная арифметика не нравится? Боишься накосячить — не юзай. Хотя, конечно, это для кода, который пишется навсегда. Менять в нем что-нибудь даже автор зачастую в силах
Если очень захотеть, то получить адресную арифметику в Go можно.
Это будет выглядеть примерно как в Rust, или, если позволите, «не как в Си».

Вы ведь не придерживаетесь мнения, что для высокоуровневого кода арифметика указателей полезна?
`unsafe.Pointer` и `uintptr` в совокупности с возможностью преобразовывать их друг в друга дадут возможность читать по указателю со смещением.

Это может быть полезно при работе, например, с сишными библиотеками.

Point в том, что там, где реально нужно использовать арифметику указателей, делать это можно. Тезис «людям не нравится адресная арифметика» не обоснован,
корректнее сказать: «большинству не нравится адресная арифметика там, где можно было обойтись без неё».

P.S. — если кто-то этим воспользуется, стоит мониторить
proposal: spec: disallow T<->uintptr conversion for type T unsafe.Pointer

Это, как минимум, сильно усложняет жизнь Garbage Collector'у и не позволяет компилятору вставить за вас проверки на выходы за границы массива, например.

Подскажите пожалуйста одну вещь. В своё время я задавал вопрос, почему существует такая вещь как not-so-nil interface pointers — ситуация, когда nil-указатель на переменную конкретного типа неявно приводится к указателю на интерфейс, который этот тип реализует. И, как результат, у нас получается fat pointer с nil data pointer и валидным vtable pointer. Следствие — nil pointer dereference в рантайме, где его не ждали. Но вопрос не в этом. Вопрос в том, что ответ тогда был "потому что в Go указатели, а не ссылки". Как раз этот ответ я и не понял.

Я тоже этот ответ не понял. Я бы сказал, что это становится понятным, если посмотреть как интерфейсы под капотом устроены (ну, мне, по крайней мере стало понятно): habrahabr.ru/post/325468/#interfeysy

Мне в общем-то тоже понятно, почему так происходит. Мне даже понятно, для чего используется эта фича. Что мне, к сожалению, осталось непонятно — почему это разрешено в виде неявного преобразования. Как по мне, сильно повышает шансы наступить на грабли. Было бы в виде явного тайп каста, так что неявный приводит к fully nil interface pointer — вопросов бы не было. Впрочем, как реализовали так реализовали. Может в v2 поменяют. Или давно есть линт, о котором я не знаю.

А покажите пример, похоже я не до конца понял проблему.

Вырожденный пример будет какой-то такой


// Note: both FirstError and SecondError implement 'error' interface
// and use some data from value when their respective Error() is called

// May return non-nil error, if code fails
func first(arg MyData) *FirstError { /* some code here */ }
// May also return non-nil error, if code fails
func second(arg MyData) *SecondError { /* some other code here */ }

func third(arg MyHugeData) error {
    if err := first(arg.DataField); err != nil {
        return err
    }
    return second(arg.DataField)
}

func fourth(arg MyHugeData) {
    if err := third(arg); err != nil {
        fmt.Println(err.Error())
    }
}

Такой код КМК вполне может случиться — "просто верни статус последней операции дальше по стеку". Во-первых, результат всегда будет трактоваться как неуспех. Во-вторых, при попытке узнать причину мы получим nil-pointer panic из по сути ниоткуда. Пример с обработкой ошибок КМК просто будет самый типичный. Я в своё время нарвался на такое поведение в другом контексте, подробностей уже не помню.
Но суть была именно такой:


  1. Есть несколько типов, приводимых к одному интерфейсу.
  2. Каждый из типов требует не-нулевой инстанс чтобы корректно реализовывать указанный интерфейс
  3. В некоем коде, допустим при передаче через канал, указатель на объект какого-либо из конкретных типов приводится к интерфейсу. При этом для канала nil pointer является абсолютно корректным положением вещей и обрабатывается на принимающей стороне как положено.
  4. На принимающей стороне не-нулевой указатель на интерфейс неожиданно оказывается очень даже нулевым, т.к. сам по себе vtable бесполезен. Результат — NPE где не ждали.
Ну да, так запутанно, конечно.
Ошибка тут, конечно очевидная — возврат ошибки по значению, а не с помощью интерфейса error из First и Second. Тоесть — это как бы и не проблема (с точки зрения компилятора), но это немного самодельный способ обработки ошибки — «А если first еще и другой тип ошибки может вернуть, а не только *FirstError)?».

Пример с обработкой ошибок просто самый наглядный. Во второй половине комментария описан чуть более сложный случай. Добавьте туда пункт 2.5:


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


Проблема именно в том, что в сколь-нибудь нетривиальном коде вполне можно получить "битый интерфейс" на ровном месте.

Поэтому решение включить указатели (без арифметики указателей, к счастью) в Go с тем же синтаксисом — было вполне логичным и естественным.


Если нет арифметики указателей, по мне было бы логичнее использовать два ключевых слова — для типов по ссылке и для типов по значению. Скажем class и record.

Указатель на один тип данных можно разыменовывать как на другой в Go? Если да, то вообще решение убрать арифметику будет странным.

Если нет, то можно было бы определить типы «указателей на» и «значений» и все.

var a int
var b pInt

pInt = class int
Sign up to leave a comment.

Articles