Любому программисту знакомо понятие "ссылка". Под этим термином обычно понимают небольшой объект, главная задача которого обеспечить доступ к другому объекту, физически расположенному в другом месте. Из-за этого ссылки удобно использовать, они легко копируются, и с их помощью очень просто получить доступ к объекту, на который эта ссылка ссылается и можно получить доступ к одним и тем же данным из разных мест программы.
К сожалению ссылки, точнее ручное управление памятью, является наиболее частой причиной возникновения различных ошибок и уязвимостей в программном обеспечении. А все попытки автоматического управления памятью с помощью различных менеджеров упираются в необходимость контролировать создание и удаление объектов, а также периодически запускать сборщик мусора, что отнюдь не положительно сказывается на производительности приложений.
Тем не менее, ссылки в той или иной форме поддерживаются во всех языках программирования, хотя под этим термином часто подразумеваются не полностью эквивалентные термины. Например, под словом "ссылка" можно понимать ссылку как адрес в памяти (как в С++) и ссылку, как указатель на объект (как в Python или Java).
Хотя встречаются языки программирования, которые пытаются решать данные проблемы за счет концепции "владения" (Rust, Аргентум или NewLang). О возможном решении этих и других имеющихся проблем со ссылками далее и пойдет речь.
Какие ссылки бывают?
Например, в языке C есть указатели, но работать с ними не очень удобно и одновременно очень опасно из-за наличия адресной арифметики (возможности напрямую изменять адрес указателя на данные в памяти компьютера). В C++ появилась отдельная сущность — reference, а в C++11 ссылки получили дальнейшее развитие, появились rvalue-ссылки.
Тогда как в С++ существует сразу несколько видов ссылок, то разработчики Python наверно специально попробовали "упростить" работу со ссылками и вообще отказались от них. Хотя де факто в Python каждый объект является ссылкой, хотя некоторые типы (простые значения) автоматически передаются по значению, тогда как сложные типы (объекты) всегда по ссылке.
Циклические ссылки
Еще существует глобальная проблема циклический (круговых) ссылок, которая затрагивает практически все языки программирования (когда объект прямо или через несколько других объектов указывает на самого себя). Часто разработчикам языка (в первую языков со сборщиками мусора) приходится идти на различные алгоритмические ухищрения, чтобы почистить пул созданных объектов от подобных "зависших" и циклических ссылок, хотя обычно эту проблему отдают на откуп разработчикам, например, в С++ есть сильные (std::shared_ptr) и слабые (std::weak_ptr) указатели.
Неоднозначная семантика ссылок
Еще не менее важной, но часто игнорируемой проблемой ссылок является семантика языка для работы с ними. Например в С/С++ для обращения к данным по ссылке и по значению используются отдельные операторы звездочка "*", стрелочка "->" и точка ".". Но работа с reference переменными в С++ происходит как с обычными переменными "по значению", хотя по факту это не так. Конечно при явной типизации С++, компилятор не даст ошибиться, но при обычном чтении кода, у вас не получится отличить reference переменную от обычной "по значению".
А вот в Python можно очень легко попутать как будет происходить передача переменой в качестве аргумента функции, по значению или по ссылке. Так как это зависит от самих данных, которые содержатся в переменной. Особой пикантности тут добавляет тот факт, что Python является языком программирования с динамической неявной типизацией, и в общем случае заранее не известно, какое значение хранится в переменной.
Кто виноват и что делать?
Мне кажется, что основная причина, по крайней мере неоднозначной семантики, это постоянный рост сложности инструментов разработки, и как следствие — усложнение и доработка синтаксиса языков программирования под новые концепции и возможности с сохранением обратной совместимости со старым legacy кодом.
А что если начать с чистого листа? Вот например, универсальная концепция управления объектами и ссылками на объекты, которая не требует от пользователя (программиста) ручного управления памятью, для которой не нужен сборщик мусора, а ошибки при работе с памятью и ссылками на объекты становятся невозможны за счет полного контроля управления памятью еще на этапе компиляции исходного кода приложения!
Термины:
- Объект — данные в памяти компьютера в машинном (двоичном) представлении.
- Переменная — человекочитаемый идентификатор в теле программы, который однозначно определяется по своему имени и идентифицирует объект (непосредственное значение объекта или ссылку на него). Переменные могут быть:
- Переменная владелец — единственная постоянная ссылка на объект со счетчиком использования объекта (shared_ptr).
- Переменная ссылка — временная ссылка на объект, которая не изменяет счетчик использования объекта (weak_ptr).
Возможные операции:
- создание новой переменной-владельца и начальная инициализация значения объекта.
- создание переменной-ссылки на существующую переменную-владельца.
- присваивание нового значения объекту по имении переменой-владельца.
- присваивание нового значения объекту на которую указывает переменная-ссылка.
- присваивание переменной-ссылке, новой ссылки другую переменную-владельца.
Пример исходного кода и условные сокращения:
- "&" — создание ссылки на переменную (создание слабого указателя)
- "*" — обращение к данным объекта (захват ссылки — преобразование слабого указателя в сильный с увеличением счетчика ссылок)
# переменные - владелец
val1 := 1;
val2 := 2;
# val1 = 1, а val2 = 2
val1 = val2; # Ошибка - владелец только один!
*val1 = *val2; # ОК - присваивается значение
# val1 = 2, а val2 = 2
# переменные - ссылки
ref1 := &val1;
ref2 := &val2;
# ref1 -> val1, а ref2 -> val2
ref1 = 3; # Ошибка - переменная является ссылкой!
*ref1 = 3; # ОК - значение присваивается "по ссылке"
# val1 = 3, а val2 = 2
# *ref1 = 3, *ref2 = 2
*ref2 = *ref1; # ОК - одно значение присваивается другому значению "по ссылке"
# val1 = 3, а val2 = 3
# *ref1 = 3, *ref2 = 3
ref1 = ref2; # ОК - одна ссылка присваивается другой ссылке
# ref1 -> val2, а ref2 -> val2
# *ref1 = 3, *ref2 = 3
*val2 = 5;
# *ref1 -> 5, *ref2 -> 5
При таком синтаксисе все становится просто, наглядно и понятно. Но если я что-то где-то упустил, напишите пожалуйста в комментарии или в личном сообщении.
З.Ы.
Как в комментариях написал unreal_undead2 "в результате в коде будут сплошные звёздочки".
Поэтому как компромисс, звездочку при обращении к данным объекта можно не указывать, если переменная — владелец используются в выражении как rvalue, т.е.:
*val1 = val2; # Так тоже можно
З.З.Ы.
После обсуждения различных возможных особенностей использования данного синтаксиса, стала очевидной необходимость получения данным "по ссылке" для полей структуры (класса). И чтобы не городить огород, имеет смысл использовать точно такой же синтаксис обращения к данным по ссылке в том числе и для полей:
class A {
A ref;
};
A owner := A();
owner.ref := &owner; # Циклическая ссылка :-)
# owner.*ref -> owner
A link := &owner;
# *link.ref - reference field
# *link.*ref -> owner
Комментарии (103)
vadimr
23.04.2024 13:08Когда вам, например, операционная система передаёт указатель на свой буфер с прочитанными из файла данными, то кто в этом случае является владельцем?
Двойную буферизацию не рассматриваем, как неэффективное решение.
rsashka Автор
23.04.2024 13:08Наверно не "в свой буфер", а сохраняет в тот буфер, который был её передан для сохранения прочитанных из файла данных.
Для ОС это обычный адрес в памяти, а для языка программирования, это либо переменная-владелец буфера, либо захваченная слабая ссылка.unreal_undead2
23.04.2024 13:08+1Я так понимаю, вопрос скорее про mmap (и дальнейшую передачу отдельных кусков в другие функции без копирования), чем read/write.
rsashka Автор
23.04.2024 13:08Вряд имеет смыл совмещать низкоуровневое ядерное управление памятью и прикладное программирование.
unreal_undead2
23.04.2024 13:08Предлагаете в прикладных программах забить на производительность и требования к ресурсам в угоду чистоте кода?
rsashka Автор
23.04.2024 13:08Предлагается отказаться от сборщика мусора и обеспечить проверку корректности работы с памятью во время компиляции приложения.
Не понял, где тут требования к ресурсам и к производительности?
unreal_undead2
23.04.2024 13:08+1Тот же mmap часто производительнее, чем функции, сохраняющие данные из файла в переданный пользовательский буфер.
rsashka Автор
23.04.2024 13:08Ну и часто ли используется mmap для чтения файлов в прикладных задачах?
unreal_undead2
23.04.2024 13:08+1В серьёзном промышленном коде, часто работающем с вводом/выводом - вполне себе используется. Даже "игрушечный" sqlite на нём живёт.
rsashka Автор
23.04.2024 13:08sqlite отнюдь не игрушечный, да и файловый ввод/вывод для него - самое узкое место.
vadimr
23.04.2024 13:08Дело же не в sqlite, а в принципиальном вопросе.
Если мы упарываемся по производительности, то в базе должен лежать механизм для передачи данных в другой контекст без копирования.
Если мы не упарываемся по производительности (т.е. в 99.9% случаев), то более чем достаточно обычного автоматического управления памятью со сборщиком мусора, и при этом не требуются никакие сильные и слабые ссылки, деструкторы и прочая хтонь.
rsashka Автор
23.04.2024 13:08Тогда странно, почему придумано более двух языков программирования, если выбор возможен лишь либо одно, либо другое, а все остальное ненужно.
vadimr
23.04.2024 13:08Во-первых, языки придумывают не только из-за ссылок. Во-вторых, эффективный сборщик мусора придумали далеко не сразу, за него спасибо джавовской отрасли.
boldape
23.04.2024 13:08+1Я как немного спец по скулайту скажу что ммап для него не принципиально нужен. До реализации вол мода его вообще не было, да и с волом в режиме эксклюзив ммап не используется. В кратце там ммап только для координации работы вол между разными процессами, но НЕ для ИО.
unreal_undead2
23.04.2024 13:08Спасибо, не знал - то есть собственно данные базы читаются/пишутся через read/write (если база в память не влезает)?
boldape
23.04.2024 13:08Оно всегда читает пишет через рид/врайт по странично, часть страниц лежит в кэше в памяти, есть возможность открыть базу полностью в памяти, размер базы не влияет на размер кэша.
unreal_undead2
23.04.2024 13:08По сравнению с нормальным базами типа Оракла или SAP HANA - игрушка. Хотя и полезная в определённых ситуациях.
vadimr
23.04.2024 13:08Это двойная буферизация, не пойдёт. Тогда проще и всё остальное по значению копировать.
Размер буфера ОС как минимум равен размеру физически читаемого блока, который прикладной программе знать неоткуда.
Batalmv
23.04.2024 13:08универсальная концепция управления объектами и ссылками на объекты, которая не требует от пользователя (программиста) ручного управления памятью, для которой не нужен сборщик мусора, а ошибки при работе с памятью и ссылками на объекты становятся невозможны за счет полного контроля управления памятью еще на этапе компиляции исходного кода приложения!
ОК, но по сути "мусор" надо собирать в конце каждого блока, в котором что-то было объявлено? А если я создал объект внутри функции, а потом его вернул? А он в свою очередь, имеет в своем составе другие объекты
По сути, без описания границ видимости и времени жизни объекта ничего не понятно :(
rsashka Автор
23.04.2024 13:08Вы совершено правы про границы видимости!
В описании я специально не стал заострять внимание на этом момент, но тут все просто. При захвате слабой ссылки, сильную ссылку можно сохранить в локальную переменную более низкого уровня. Но нельзя на в переменную на своем уровне или вернуть на уровень выше.
owner := 123; ref := & owner; { local := *ref; # В local будет сильная ссылка на owner во вложенном блоке }
Batalmv
23.04.2024 13:08Ну теперь вы пришли к концепции сильных/слабых ссылок в *nix файловых системах. И по сути, к тому же сборщику мусора, когда чисто сильных ссылок стало равно 0 :)
Все равно, без точного описание границ видимости и критериев "удаления объекта" вам концепции на построить
rsashka Автор
23.04.2024 13:08Удаление объекта, когда число сильных ссылок стало равно 0, это не сборщик мусора, а работа обычного деструктора.
YanTsys
23.04.2024 13:08А как же
ref1 := &val1+1;
rsashka Автор
23.04.2024 13:08+1Не ... ненужно никакой адресной арифметики.
Sachavr
23.04.2024 13:08+1"Не ... ненужно никакой адресной арифметики." - Золотые слова. Немного не в тему данного ответа, выражение зацепило, хотел сказать про низкоуровневую адресацию. По идее програмисту не надо знать про кэши, страницы и прочее аппаратное, т.к. везде разное, это дело процессора, драйверов или ОС (в крайнем случае). Программисту нужно просто выразить то что он хочет, желательно на человеческом языке. И когда я смотрю на эти закорючные символы и нечитаемую семантику в разных языках, то понимаю что будет много ошибок и непоняток. Почему нельзя написать язык где всё на человеческом языке, ведь всё равно сегодняшние закорючки нужно прогонять через компилятор. Самое сложное перевести высокий уровень в код для проца, ведь сегодняшнее железо не умеет работать с высоким уровнем и даже память ОС не может защитить аппаратно, Поэтому тяжол труд программиста в наше время. Хочется о высоком а приходится на низком. Это так, мысли вслух.
rsashka Автор
23.04.2024 13:08Самое сложное перевести высокий уровень в код для проца
Это как раз самое простое. Самое сложное, это записать нужный алгоритм в семантике языка, а потом искать и исправлять ошибки.
unreal_undead2
23.04.2024 13:08+1По идее програмисту не надо знать про кэши, страницы и прочее аппаратное
Если надо получить максимум производительности - очень даже надо.
И когда я смотрю на эти закорючные символы
Эффективное использование подсистемы памяти чаще выражается на уровне алгоритма, а не в каких то нюансах языка.
Mingun
23.04.2024 13:08+1ref1 = 3; # Ошибка - переменная является ссылкой! ... ref1 = 5; # val1 = 5, а val2 = 5
А вы говорите, все наглядно-очевидно... (не спрашиваю о том, как в
val1
иval2
оказалось одинаковое значение... когда отval1
уже все ссылки отцепились). Если уж нужна очевидность, то пусть ссылки всегда носят с собой дополнительный символ, говорящий -- "я ссылка". А доступ к данным -- это убирание этого символа. Это и символично -- убираем уровень косвенности -- получаем данные. Хотя тогда могут возникнуть проблемы с обобщенным кодом -- даже если он не зависит от того, ссылка там или объект, синтаксис чисто формально может требоваться разный.rsashka Автор
23.04.2024 13:08Спасибо! Это действительно косяк в примере. Тут должно быть по другому:
... ref1 = ref2; # ОК - одна ссылка присваивается другой ссылке # ref1 -> val2, а ref2 -> val2 *ref1 = 5; # *ref1 -> 5, *ref2 -> 5
andreymal
23.04.2024 13:08+5А вот в Python можно очень легко попутать как будет происходить передача
переменой в качестве аргумента функции, по значению или по ссылке.В питоне всё всегда передаётся по ссылке, просто некоторые примитивные/иммутабельные типы не предоставляют официальных способов изменить себя.
Но если очень хочется...
import ctypes def fuck(x: int) -> None: ctypes.memset(id(x) + 24, 255, 4) def main() -> None: a = int("1000") print(a) # 1000 fuck(a) print(a) # 4294967295 main()
rukhi7
23.04.2024 13:08+1любая переменная в конечном итоге это адрес некоторой области в памяти (+некоторая дополнительная информация об этой области и о том в каком виде там данные лежат, в зависимости от языка).
Отличается способ копирования переменной:
либо мы создаем еще один описатель этой области памяти, то есть переменной. Описатель, который обязательно содержит адрес этой области памяти,
либо мы копируем содержимое этой области памяти в область памяти с такими же параметрами.
Любая попытка абстрагироваться от того что программы работают с памятью и с ее адресами это очередная попытка всех запутать, мне кажется. Это как в электрике пытаться абстрагироваться от закона Ома, мне кажется. Типа я вот буду провода соединять супер клеем и лампочка у меня загорится более безопасно.
rsashka Автор
23.04.2024 13:08+1Тогда зачем абстрагироваться от машинных инструкций? Ведь все равно выполнятся именно они?
Зачем абстрагироваться от работы транзисторов или даже потоков электронов, ведь в конечном итоге именно эти процессы обеспечиваются работу любой вычислительной машины?Человек не может объять необъятное, и при постоянном увеличении сложности решаемых задач (количества обрабатываемых данных) ему требуется более совершенные инструменты. Например, чтобы не заботиться о ручном управлении памятью и одновременно не тратить лишние ресурсы за использование сборщика мусора за автоматизацию данного процесса.
rukhi7
23.04.2024 13:08Зачем абстрагироваться от работы транзисторов или даже потоков электронов, ведь в конечном итоге именно эти процессы обеспечиваются работу любой вычислительной машины?
но аналогию можно продолжить конечно:
многие слушали и слушают транзисторные приемники и фактически управляют ими не зная и не понимая что такое транзисторы, но врядли такие специалисты могут разрабатывать эти транзисторные приемники.
Вопрос то простой мы про какого уровня специалистов говорим? про пользователей или про разработчиков? Про тех кому два байта переслать или про тех кто серезные какие-то алгоритмы реализует с серьезными требованиями по производительности, например.
rukhi7
23.04.2024 13:08вот если вы будете код портировать с Х86 платформы на ARM платформу, получится у вас от абстрагироваться от машинных инструкций?
Это же в общем то вопрос области разработки в том числе, или вы не согласны?
rsashka Автор
23.04.2024 13:08Самое интересное в вашем вопрос то, что мне приходится работать как X86, так и с ARM платформой и писать такой код, который приходится в дальнейшем портировать. Поэтому абстракция для разработчиков, это все.
Но уровень абстракции зависит от задачи. Например в одном случае я могу взять LLVM и выполнять какой угодно код на любой платформе, но если мне нужно запустить приложение на микроконтроллере, но на него уже никакой LLVM не влезет, поэтому приходится создавать нативное приложение и заниматься кросс компиляцией.
Есть и еще проблема, связанная с уровнем зрелости технологии. Вряд ли кому придет в голову обкатывать и отлаживать новый язык программирования на уровне драйверов ядра операционной системы, до тех пор, пока он не зарекомендует себя в качестве надежного решения хотя бы на обычных прикладных задачах.
Поэтому мне очень нравится подход Python и Java (любой объект - это ссылка на область в памяти), очень удобно не заботится о распределении памяти, но мне очень не нравится плата за это - необходимость использования сборщиков мусора и лишние накладные расходы. Мне нравится подход С++ за его минимализм оверхеда, но все убивает необходимость ручного управления памятью, в которой очень легко ошибиться.
Поэтому я пытаюсь обкатать и обсудить описанную выше концепцию, а потом попробую её реализовать уже в конкретном языке программирования.
rukhi7
23.04.2024 13:08очень нравится подход Python и Java (любой объект - это ссылка на область в памяти),
...
Мне нравится подход С++ за его минимализм оверхеда, но все убивает необходимость ручного управления памятью, в которой очень легко ошибиться.
Я на С# пишу много теперь, сборщик мусора там вроде как не является необходимостью, никто не мешает удалить объект вручную, насколько я знаю на Java также можно действовать.
Главная проблема С++ по моему хидера и ручное описание интерфейсов-отсутствие полной рефлексии по типам. Для управления памятью все уже придумано например COM-объекты. Проблема только с оформлением через те же хидера, по моему. Ну и однажды применив технику надо не лениться ее следовать последовательно, а не так что тут у нас было так, а потом как попало.
unreal_undead2
23.04.2024 13:08Для большей части прикладного кода компилятор всё абстрагирует - проблемы будут скорее не с ISA, а со слабой моделью памяти, что решается высокоуровневыми средствами. Но, скажем, спортировать на новую архитектуру Javascript движок - это уже задачка интересная )
rukhi7
23.04.2024 13:08спортировать на новую архитектуру Javascript движок - это уже задачка интересная
эта задача довольна абстрактная, так как какого типа, объема, производительности, ... скрипты вы на этом движке собираетесь запускать - большой вопрос.
А вот если вы будете портировать какой-то Mpeg аудио декодер, или не дай бог видео декодер, тут уже достаточно хорошо известны требования которые происходят из параметров сигнала - звука/картинки который надо воспроизвести. И вряд ли получится обойтись без знания аппаратных особенностей платформы и исходной и целевой для портирования.
unreal_undead2
23.04.2024 13:08так как какого типа, объема, производительности, ... скрипты вы на этом движке собираетесь запускать - большой вопрос
Да просто броузиться, чтобы всяческие Google Docs/Maps адекватно работали.
какой-то Mpeg аудио декодер, или не дай бог видео декодер
Согласен, там нужно как минимум доступные SIMD инструкции/регистры запользовать. Вроде есть переносимые подходы (скажем, Google Highway) - но не знаю, насколько эффективный код они генерируют.
rukhi7
23.04.2024 13:08Вроде есть переносимые подходы (скажем, Google Highway) - но не знаю, насколько эффективный код они генерируют.
они генерируют код для каких-то общих случаев, максимальная эффективность достигается именно в специфических для данной платформы решениях, то есть в уникальных для данной платформы решениях.
Переносимые подходы значит универсалиные в каком-то смысле, это противоположность уникальных подходов, эти области решений практически не пересекаются, лишь иногда они могут соприкасаться - это я наблюдал.
Эффективность локального браузера частично можно обеспечить за счет локальных-же специфичных для данной платформы модулей, еще DirectX есть который очень хорошо заточен под эффективность, так как его основная идея близость к аппаратным возможностям, но это все вне области высокоуровневых языком таких как JAVA и Питон.
unreal_undead2
23.04.2024 13:08они генерируют код для каких-то общих случаев
Вот и интересна применимость. В самых простых случаях (скажем, сложить два массива) и компилятор вполне справляется, запользовать особенности конкретной реализации - только руками на ассемблере или интринсиках, получается Highway где то посередине, только вот насколько она широкая...
Эффективность локального браузера частично можно обеспечить за счет локальных-же специфичных для данной платформы модулей
Но всё таки для сайтов с нетривиальными скриптами главное - эффективная JIT компиляция JS кода, при этом llvm там не особо применим, так что backend для новой архитектуры придётся писать руками.
rukhi7
23.04.2024 13:08только руками на ассемблере или интринсиках, получается Highway где то посередине, только вот насколько она широкая...
вот я руками на ассемблере и делал, максимум что можно потом сделать с этим специальным решением на ассемблере, написать программу которая будет генерировать этот ассемблер для этого КОНКРЕТНОГО алгоритма, как-то параметризованного для более менее похожих наборов инструкций процессора и некоторых параметров алгоритма.
То есть КАЖДЫЙ новый алгоритм требует такой специальной работы это нельзя автоматизировать пока все возможные алгоритмы для всех принципиально разных платформ не будут написаны и сведены в какую-то общую базу алгоритмов.
NeoCode
23.04.2024 13:08+1Т.е. вы предлагаете все переменные (в том числе те которые "по значению") признать ссылками и использовать унифицированный синтаксис? Как уже заметили, в таком коде будет слишком много разыменований (звездочек). А любые попытки что-то упростить и срезать углы нарушат заявляемую стройность концепции (хотя я не уверен в стройности концепции изначально).
Вообще существуют два подхода: высокоуровневый и низкоуровневый. При высокоуровневом нам все равно как хранится объект - по значению или по ссылке, мы абстрагируемся от способа хранения и работаем просто с объектами. Так устроены Java и C#. Единый синтаксис доступа, никаких звездочек и стрелочек - только прямой доступ и доступ к полям через точку. Наверное, для специальных операций (таких как манипуляции с самими указателями, а не с объектами) можно придумать специальный синтаксис, или даже специальные встроенные в язык функции.
Второй подход - низкоуровневый, в нем в программе явно задается способ хранения объекта. Лучший представитель этого подхода - язык Си. Явные указатели, явное разыменование, явное взятие адреса, всё тоже просто и понятно, но явно. Иногда чуть больше звездочек, но это цена явности и прозрачности кода. В Rust тоже вроде бы все кодируется в явном виде. Где-то рядом Go, хотя там чуть срезали некоторые некритические углы.
А иногда делаются попытки совместить. Как в С++, где есть и явные указатели, и неявные ссылки. Иногда это удобно, иногда кажется избыточным. Посмотрите еще как забавно и в то же время мозгодробительно сделано в языке CForAll - они в дополнение к сишным указателям взяли концепцию ссылок из с++ и вывернули ее наизнанку, построив аналогию указателей на указатели.
int x, *p1 = &x, **p2 = &p1, ***p3 = &p2, &r1 = x, &&r2 = r1, &&&r3 = r2; ***p3 = 3; // change x r3 = 3; // change x, ***r3 **p3 = ...; // change p1 &r3 = ...; // change r1, (&*)**r3, 1 cancellation *p3 = ...; // change p2 &&r3 = ...; // change r2, (&(&*)*)*r3, 2 cancellations &&&r3 = p3; // change r3 to p3, (&(&(&*)*)*)r3, 3 cancellations
ИМХО, пример того, когда сделали чисто для языковой симметрии в ущерб читаемости и практической применимости. Я считаю, что указатель и ссылка, при всей внутренней схожести, на уровне языкового дизайна все-же разные конструкции: указатель - мощный низкоуровневый инструмент для построения явных связей на уровне адресов; ссылка - высокоуровневая абстракция, предназначенная как раз для того чтобы уйти от адресов, явных размыменований и прочего.
rsashka Автор
23.04.2024 13:08Большое спасибо за очередной развернутый комментарий от вас!
По вашей терминологии предлагается "среднеуровневый" подход в управлении ссылками, т.е. все хранится как объект - "по значению или по ссылке, мы абстрагируемся от способа хранения и работаем просто с объектами.", но с возможностью явного использования данной ссылки в низкоуровневых языках (С/С++) напрямую без каких либо оберток, надстроек или конвертации.
Плюс унификация синтаксиса при работе со ссылками (а чтобы не было все в звездочках, разрешить их не писать, если переменная - владелец используются как rvalue), т.е. самый типичный случай использования.
К тому в эту канву очень удачно будет вписываться вообще полный контроль за ссылками, в том числе и за межпоточным взаимодействием и все это опять же во время компиляции, а такого нет даже в Rust :-)
posledam
23.04.2024 13:08В C# не "всё есть объект". Есть вполне себе значимые типы (структуры) и ссылочные типы (классы). Значимые типы передаются по значению, ссылочные по ссылке (указателю). При этом можно иметь ссылку на значение или указатель. И всё это разнообразие доступно, понятно и работает без звёздочек, амперсандов и стрелочек типа ->.
Какой смысл возвращаться к специальному ссылочно-значимому синтаксису, совершенно непонятно.
rsashka Автор
23.04.2024 13:08При работе с сылками в С# вы на уровне синтаксиса не можете отличить аргумент по ссылке от аргумента по значению.
posledam
23.04.2024 13:08+2А ещё на уровне синтаксиса я не могу отличить локальную переменную от типа, свойства или поля, также не могу однозначно сказать какого типа переменная.
rsashka Автор
23.04.2024 13:08Обращение к локальной переменной легко отличается от доступа к полю (свойству) на уровне синтаксиса.
NeoCode
23.04.2024 13:08Не отличается: вот пример, обращение к локальной переменной (аргументу функции) и к полю соврешенно одинаковое (хотя мы знаем что на низком уровне в первом случае работа через указатель this, во втором - просто доступ к стеку). Чтобы отличить, нужно смотреть место объявления. А это может быть где-то далеко в коде (хорошо современные IDE выводят подсказки по наведению мыши на переменную).
class Foo { int x; void bar(int y) { x = 10; y = 10; } }
rsashka Автор
23.04.2024 13:08Я конечно не спец по С#, но такой синтаксис, который мешает в одну кучу поля класса, локальные переменными и ссылки мне однозначно не нравится.
mmatvey1123
23.04.2024 13:08+1Вроде во многих языках можно так обращаться к полям класса. Та же java, c++.
Мне так же не нравится данный синтаксис. Именно по этому есть ключевое слово this, явно показывающее, что происходит обращение к полю класса.
nv13
23.04.2024 13:08+1И всё это разнообразие доступно, понятно и работает без звёздочек, амперсандов и стрелочек типа ->
А мне не нравится. Раньше с этими загогулинами понятно всегда было где ты и чем тебе это грозит, а теперь можно так а можно по новому. Слишком уж диверситифицировано)
boldape
23.04.2024 13:08+1О мне тут кармы налили, так что я могу больше всякого писать в единицу времени, но не на долго конечно.
Так вот, первое я чёт не особо вижу разницы с аргентумом в плане указателей который уже хоть как то работает, второе я так и не понял какую проблему решаем? Универсальный синтаксис - ассемблер. Безопасность памяти эта идея не решает за исключением юз афтер фрии и дабл делет, но эти проблемы не очень сложно чинить руками. Где решение проблемы дата рэйсов? Ну и производительность, локать шаред поинтер для каждого доступа к данным это точно не взлетит там где нужна ну хоть какая то производительность, по сути почти весь кэш данных просто отключается.
Я щас скажу то, что многие не хотят слышать шаред поинтеры нужны - да почти никогда. Шон Пэрент гуглите он вам в подробностях объяснит, что с ними не так (спойлер да почти все с ними не так).
Язык VALE начинался как раз с этих идей там люди точно также заменили все указатели на шаред и вик, а потом поняли что просто юник и роу поинтеров достаточно почти всегда, ну а дальше они ещё глубже ушли.
Кстати Валя куда идейно более проработана чем аргентумом и тем более ваша идея. Так что я вашу идею не куплю.
unreal_undead2
То есть в результате в коде будут сплошные звёздочки. И всё таки непонятно, что делается с циклическими ссылками (скажем, работа с графами общего вида).
rsashka Автор
Циклические ссылки уже не являются проблемой, так как все они будут слабыми, а звездочка, это как раз и есть оператор преобразования слабой ссылки в сильную на время операции. Точнее обращения к данным (для слабой ссылки - преобразование в сильную и обращение к данным, а для сильной - сразу обращение к данным).
unreal_undead2
Соответсвенно в таком случае ссылки в плане контроля за временем жизни ничем не лучше обычных указателей. Или предлагается завести некую искуственную сущность, владеющую всеми вершинами графа через сильные ссылки?
rsashka Автор
А никакой новой сущности и не требуется. Вершина каждого графа - это переменная владелец.
unreal_undead2
Ничего не понял - предлагается под каждую вершину заводить отдельную переменную?
rsashka Автор
Что значит "заводить"? Это может быть обычный std::shared_ptr с собственным счетчиком ссылок.
unreal_undead2
Всё таки непонятно, как оно работает, если, скажем, есть три вершины (A, B, C) и каждая ссылается на все остальные, после чего мы убираем рёбра A-B и A-C, при этом в глобальных структурах есть ссылка только на A.
pda0
Да не вопрос. Всё легко решается. Пляшем от того, что у каждого значения должен быть владелец-литерал, без которого значение сразу удаляется. Для значений внутри контейнеров владельцем-литералом всех значений может быть литерал контейнера.
Чтобы избежать двусмысленности владельцем разрешаем быть только одному литералу. Но остальные могут "одалживать" значение. Или забирать его себе на совсем. И...
Так, минуточку!.. :-D
rsashka Автор
Да, фактически это Borrow checker в Rust :-D
unreal_undead2
Насколько слышал, написание сложных структур данных с нетривиальными ссылками между элементами на Rust - это как раз таки постоянная борьба с borrow checker. Но пока руками не трогал.
rsashka Автор
Вы правильно слышали. Поэтому я упомянул Rust как источник вдохновения для самой идеи, но писать на нем реальные приложения - для меня сплошная мука.
sdramare
Ну в реальности на сложных структурах просто отказываются от bc в пользу счетчиков ссылок(Rc/Arc).
rsashka Автор
Но в этому случае появляется возможность получить циклическую ссылку!
sdramare
Появляется, но что делать. Ну если есть серьезные опасения, то можно Weak использовать. В borrow checker уровня компиляции принципиально нельзя записать ссылку на самого себя, потому что это потребует одновременно взять структуру на изменение и одалживание(borrow), что запрещенно правилами. Более того, когда еще асинхронный код требуется и clone делать дорого, то в реальных приложениях часто в какой-то момент вылезает что-то типа Arc<Mutex<HashMap>>, такое вот неприятно отличие практики от теории.
rsashka Автор
Делать полный контроль над ссылками, в том числе и за расшаренными между разными потоками, а не только за владельцами объектов. В этом случае и циклических ссылок можно будет избежать и проверки будут выполняться во время компиляции.
unreal_undead2
В смысле избежать? Если есть миллион равноправных объектов с перекрёстными ссылками друг на друга, циклические ссылки возникают из сути задачи - так что вопрос не как их избежать, а что с ними делать.
rsashka Автор
Не совсем так. Есть же два типа ссылок (сильные и слабые), но проблему циклических ссылок создают только сильные ссылки (владельцы объектов). Тогда как перекрестные слабые ссылки (weak_ptr) висящих ссылок не создают, т.к. не владеют ими.
unreal_undead2
И какие ссылки использовать между равноправными объектами? Никто из них другими не владеет, но когда на какое то подмножество объектов нет ссылок снаружи (возможно через другие объекты), его хочется уничтожать, при этом явная ссылка снаружи может быть только на один (ничем не выделенный) объект.
rsashka Автор
Сильные ссылки могут быть только у владельцев объекта (как borrow checker в Rust), но их можно копировать в локальные переменные более низкого уровня (т.е. без обязательного перемещения владельца, как это реализовано в Rust), так как после завершения области видимости локальной переменной, её владеющая ссылка будет удалена.
Во всех остальных случаях (в том числе и для равноправных объектов) можно делать только слабые ссылки и для работы с объектом "по ссылке" будет требоваться их захват.
А чтобы не делать захват слабой ссылки и проверку на успешность операции при каждом действии, можно захватить слабую ссылку в локальную переменную, как описано в первом абзаце.
unreal_undead2
Соответсвенно в таком подходе сильная ссылка будет одна - из какой то глобальной структуры на одну из вершин графа. Как контролировать, что при уничтожении какого то ребра подграф не виден снаружи и его надо уничтожить?
rsashka Автор
Так вы же сами написали сильная ссылка только одна (точнее владелец объекта только один), а самих ссылок может быть несколько, но за удаление объекта отвечает счетчик ссылок, Все это - классический std::shared_ptr
unreal_undead2
Сильная ссылка - только на одну вершину графа, на все остальные - только ссылки из других вершин (то есть слабые).
rsashka Автор
Сильная ссылка, это та, которая увеличивает счетчик владения.
Сильные ссылки могут быть и в других вершинах графа, но только если они создаются для более низких уровней и будут гарантированно освобождены до освобождения своего создателя (чтобы избежать циклической зависимости)
unreal_undead2
Я же говорю - все вершины равноправны, нет никакой иерархии.
rsashka Автор
Они не моту быть равноправны, так как есть вершина графа, а другие ссылки могу располагаться на разных уровнях. Поэтому о полном равноправии ссылок в принципе речи быть не может.
Равноправными ссылками могут быть только ссылки на одному уровне иерархии, и вот для них и введен запрет на копирования сильных ссылок между собой, чтобы не создавать циклическую зависимость. Во всех остальных случаях копирование сильных ссылок не приводит к проблемам.
sdramare
Ну ваш запрет на копирование "сильных" ссылок между собой будет не лучше(на деле будет хуже), чем запрет в расте на одновременное взятие ссылки на запись и чтение. Попробуйте в рамках вашей грамматики написать двухсвязный список с операциями добавления, удаления и перестановки элементов.
rsashka Автор
Я не понимаю, в чем там может быть сложность?
sdramare
Если за '
&'
стоит счетчик ссылок, тоLeaf head;
head.next = &head;
циклическая ссылка и лик память.
Если за '
&'
не стоит счетчик ссылок, тоLeaf head;
Leaf tail;
head.next = &tail;
return head
prev удаляется и получается dangling pointer
rsashka Автор
Нет, не стоит, так как это объекты одного уровня и ссылка может быть только слабой.
Вы возвращаете из функции ссылку на локальную переменную, поэтому это действительно dangling pointer. Вот только для получения данных указателя по правилам синтаксиса вам необходимое его "захватить" (оператор "звездочка"), т.е. превратить weak_ptr в shared_ptr. Поэтому тут у вас ошибка в логике, а при работе с памятью ошибки не будет.
Хотя такие логические ошибки легко выявлять во время анализа AST, так что скорее всего эта ошибка будет выявлена еще на этапе компиляции.
sdramare
Вы возвращаете из функции ссылку на локальную переменную
А как вернуть структуру по значению тогда?(это к вопрос о читабельность синтаксиса).
Вот только для получения данных указателя по правилам синтаксиса
Причем тут это? У меня есть функция create_list, которая должна вернуть мне структуру Leaf. Подскажите, что мне надо писать?
Поэтому тут у вас ошибка в логике, а при работе с памятью ошибки не будет.
Давайте даже рассмотрим случай когда все ссылки
Leaf head = new Leaf(); //ссылка, объект в куче
Leaf tail = new Leaf(); //ссылка, объект в куче
head.next = &tail;
return head // или что тут у вас надо написать, *head?
В какой момент времени будет удален
tail?
Что при этом будет в полеnext
?rsashka Автор
Да, этим вы возвращаете сильную ссылку, а фактически владельца объекта или std::move, если так будет проще.
Но в этом случае вы можете вернуть только объект head, а вот ссылка на tail протухнет, так как владельцем объекта является локальная переменная, которая к моменту возврата из функции уже будет освобождена, так как закончится её область действия.
UPD. Тут скорее всего нужно создавать какой нибудь контейнер для хранения всех листьев списка и возвращать по значению именно его, а не отдельный элемент.
sdramare
Но в этом случае вы можете вернуть только объект head, а вот ссылка на tail протухнет
Т.е. это будет dangling pointer, как я и написал. Следовательно, нам нужно вручную контролировать чтобы такая ситуация не произошла. Но ваша статья начинается с тезиса
К сожалению ссылки, точнее ручное управление памятью, является наиболее частой причиной возникновения различных ошибок и уязвимостей в программном обеспечении.
Так зачем нужна ваша грамматика, если она не решает проблему, которую вы же озвучили буквально в начале вашей статьи.
Тут скорее всего нужно создавать какой нибудь контейнер для хранения всех листьев списка
Интересно как он будет выглядеть, с учетом что одной структуре нельзя иметь сильную ссылку на другую(или можно? А если тогда цикл получится?) Напишите, интересно посмотреть. Ну и похоже что мы начинаем выяснять, как вы сказали,
в чем там может быть сложность
rsashka Автор
Раз вы не можете вернуть контейнер, значит его нужно передать снаружи, как аргумент при вызове функции. Этот объект-контейнер будет более высокого уровня, а значит сможет (по правилам синтаксиса) содержать сами объекты, а не только слабые ссылки.
sdramare
Даже если это сделать, это никак не решает проблему dangling pointer. Вот у вас
head.next = &tail;
Положит программист этой фунции в контейнер tail или забудет и положит только head - получается как повезет. Еще хуже, что контейнер мутабельный, а значит tail можно из него удалить и опять dangling pointer. Как-будто бы вашу грамматику нужно дополнять проверками на владение, лайфтаймы и мутабельностью, но тогда получится Rust.rsashka Автор
Обязательно! Более того, я хочу сделать полный контроль ссылок, включая автоматическую межпотоковую синхронизацию, поэтому Rust в любом случае не получится :-)
sdramare
Как вы этого избежите, если вы не знаете в какой момент времени будет создана ссылка?
Вот у вас в одном потоке происходит
let ref_mt2 := &&ref_mt; #
А в другом в этот же момент
ref_mt
удаляется. Как вы это будете реализовывать? Как вы на уровне комплияции поймете в какой момент в другом потоке возьмут ссылку на вашу структуру? А если там будет условие, которое может никогда не отработать? В результате всеравно к счетчику придете.rsashka Автор
В данном случае, не к счетчику, а объекту синхронизации. Точнее к счетчику, но после захвата объекта синхронизации доступа.
Поэтому все верно, объект синхронизации будет безусловно нужен. Но он будет создаваться и использоваться автоматически при работе с данной ссылкой, а не вручную, когда его можно забыть или ошибиться в логике работы.
sdramare
Ну если будет счетчик, явный или нет, то всеравно потенциально будет проблема циклических ссылок.
Вот структура
struct Node{ sub_node: &&Node}
В какой момент вы будете принимать решение о том, что ее можно удалить?
rsashka Автор
Простой ответ "Когда счетчик ссылок 0" :-D
Другое дело, как гарантировать отсутствие циклических ссылок?
sdramare
Нет никакого простого ответа. Задача об определении времени жизни объекта на этапе комплиляции в общем случае сводится к задачае "остановки" и по-этому не решаема. А решение ее на рантайме счетчиками всегда будет порождать проблему цикличности. Запреты на копирование сильных ссылок(по сути запрет move семантики для структур) для обхода проблемы приведут к невозможности описать топологию связных графов в такой грамматике. Альтернативно счетчикам можно под капот засунуть другой вид аллокатора, например, через арены делать, но тогда не понятно зачем в принципе нужна эта грамматика, можно просто адресовать все без семантики ссылок, как в GC языках.
rsashka Автор
На этапе компиляции я решаю проблему не подсчета ссылок (это действительно возможно только во время выполнения). Решается проблема циклических ссылок. И запрет копирования сильных ссылок действует только для равноправных ссылок, а move семантика тут совершенно не причем, так как она это вообще не принимает во внимание.
Mingun
Ну а в расте вы явно этот счетчик запрашиваете, оборачивая все в
Arc
, а если не сделаете, получаете ошибку компиляции. То есть в расте есть выбор (обернуть вArc
или подумать, как без этого обойтись), а у вас нет (сразу автоматом оборачиваете).rsashka Автор
Да, все так. Только для межпотокового взаимодействия требуется не счетчик, а объект синхронизации (если мы говорит об этом). А так да, счетчик является встроенным в механизм управления ссылками и работа с с ним происходит автоматически.
Просто я не могу придумать ситуации, когда есть расшаренный между потоками объект (точнее ссылка), но объект синхронизации доступа отсутствует.
Mingun
Правильно, такой ситуации нет. Но так ведь смысл в том, а нужно ли вообще создавать такую ситуацию, или можно создать другую, где ничего шарить не нужно. Rust поощряет подумать, а подумав -- явно выразить в коде намерение поступить именно так.
rsashka Автор
Смысл в том, чтобы программист думал там, где компьютер (компилятор) не сможет сделать правильно без его участия. Если не нужно шарить ссылку, создавай ссылку без возможности шаринга, и компилятор именно это и будет проверять.
rsashka Автор
Вы правы насчет звездочек! Если их писать всегда, то фактически звездочка будет использоваться при использовании значения у каждой переменной. Наверно можно их не писать, если переменная - владелец используются как rvalue.