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


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


Тем не менее, ссылки в той или иной форме поддерживаются во всех языках программирования, хотя под этим термином часто подразумеваются не полностью эквивалентные термины. Например, под словом "ссылка" можно понимать ссылку как адрес в памяти (как в С++) и ссылку, как указатель на объект (как в 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)


  1. unreal_undead2
    23.04.2024 13:08
    +1

    *val1 = *val2; # ОК - присваивается значение

    То есть в результате в коде будут сплошные звёздочки. И всё таки непонятно, что делается с циклическими ссылками (скажем, работа с графами общего вида).


    1. rsashka Автор
      23.04.2024 13:08

      Циклические ссылки уже не являются проблемой, так как все они будут слабыми, а звездочка, это как раз и есть оператор преобразования слабой ссылки в сильную на время операции. Точнее обращения к данным (для слабой ссылки - преобразование в сильную и обращение к данным, а для сильной - сразу обращение к данным).


      1. unreal_undead2
        23.04.2024 13:08

        так как все они будут слабыми

        Соответсвенно в таком случае ссылки в плане контроля за временем жизни ничем не лучше обычных указателей. Или предлагается завести некую искуственную сущность, владеющую всеми вершинами графа через сильные ссылки?


        1. rsashka Автор
          23.04.2024 13:08

          А никакой новой сущности и не требуется. Вершина каждого графа - это переменная владелец.


          1. unreal_undead2
            23.04.2024 13:08

            Ничего не понял - предлагается под каждую вершину заводить отдельную переменную?


            1. rsashka Автор
              23.04.2024 13:08

              Что значит "заводить"? Это может быть обычный std::shared_ptr с собственным счетчиком ссылок.


              1. unreal_undead2
                23.04.2024 13:08

                Всё таки непонятно, как оно работает, если, скажем, есть три вершины (A, B, C) и каждая ссылается на все остальные, после чего мы убираем рёбра A-B и A-C, при этом в глобальных структурах есть ссылка только на A.


                1. pda0
                  23.04.2024 13:08
                  +1

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

                  Чтобы избежать двусмысленности владельцем разрешаем быть только одному литералу. Но остальные могут "одалживать" значение. Или забирать его себе на совсем. И...

                  Так, минуточку!.. :-D


                  1. rsashka Автор
                    23.04.2024 13:08

                    Да, фактически это Borrow checker в Rust :-D


                    1. unreal_undead2
                      23.04.2024 13:08
                      +1

                      Насколько слышал, написание сложных структур данных с нетривиальными ссылками между элементами на Rust - это как раз таки постоянная борьба с borrow checker. Но пока руками не трогал.


                      1. rsashka Автор
                        23.04.2024 13:08

                        Вы правильно слышали. Поэтому я упомянул Rust как источник вдохновения для самой идеи, но писать на нем реальные приложения - для меня сплошная мука.


                      1. sdramare
                        23.04.2024 13:08
                        +1

                        Ну в реальности на сложных структурах просто отказываются от bc в пользу счетчиков ссылок(Rc/Arc).


                      1. rsashka Автор
                        23.04.2024 13:08

                        Но в этому случае появляется возможность получить циклическую ссылку!


                      1. sdramare
                        23.04.2024 13:08
                        +1

                        Появляется, но что делать. Ну если есть серьезные опасения, то можно Weak использовать. В borrow checker уровня компиляции принципиально нельзя записать ссылку на самого себя, потому что это потребует одновременно взять структуру на изменение и одалживание(borrow), что запрещенно правилами. Более того, когда еще асинхронный код требуется и clone делать дорого, то в реальных приложениях часто в какой-то момент вылезает что-то типа Arc<Mutex<HashMap>>, такое вот неприятно отличие практики от теории.


                      1. rsashka Автор
                        23.04.2024 13:08

                        Появляется, но что делать.

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


                      1. unreal_undead2
                        23.04.2024 13:08
                        +1

                         и циклических ссылок можно будет избежать

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


                      1. rsashka Автор
                        23.04.2024 13:08

                        Не совсем так. Есть же два типа ссылок (сильные и слабые), но проблему циклических ссылок создают только сильные ссылки (владельцы объектов). Тогда как перекрестные слабые ссылки (weak_ptr) висящих ссылок не создают, т.к. не владеют ими.


                      1. unreal_undead2
                        23.04.2024 13:08

                        И какие ссылки использовать между равноправными объектами? Никто из них другими не владеет, но когда на какое то подмножество объектов нет ссылок снаружи (возможно через другие объекты), его хочется уничтожать, при этом явная ссылка снаружи может быть только на один (ничем не выделенный) объект.


                      1. rsashka Автор
                        23.04.2024 13:08

                        Сильные ссылки могут быть только у владельцев объекта (как borrow checker в Rust), но их можно копировать в локальные переменные более низкого уровня (т.е. без обязательного перемещения владельца, как это реализовано в Rust), так как после завершения области видимости локальной переменной, её владеющая ссылка будет удалена.

                        Во всех остальных случаях (в том числе и для равноправных объектов) можно делать только слабые ссылки и для работы с объектом "по ссылке" будет требоваться их захват.

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


                      1. unreal_undead2
                        23.04.2024 13:08
                        +1

                        Соответсвенно в таком подходе сильная ссылка будет одна - из какой то глобальной структуры на одну из вершин графа. Как контролировать, что при уничтожении какого то ребра подграф не виден снаружи и его надо уничтожить?


                      1. rsashka Автор
                        23.04.2024 13:08

                        Так вы же сами написали сильная ссылка только одна (точнее владелец объекта только один), а самих ссылок может быть несколько, но за удаление объекта отвечает счетчик ссылок, Все это - классический std::shared_ptr


                      1. unreal_undead2
                        23.04.2024 13:08

                        Сильная ссылка - только на одну вершину графа, на все остальные - только ссылки из других вершин (то есть слабые).


                      1. rsashka Автор
                        23.04.2024 13:08

                        Сильная ссылка, это та, которая увеличивает счетчик владения.

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


                      1. unreal_undead2
                        23.04.2024 13:08

                        Я же говорю - все вершины равноправны, нет никакой иерархии.


                      1. rsashka Автор
                        23.04.2024 13:08

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

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


                      1. sdramare
                        23.04.2024 13:08

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


                      1. rsashka Автор
                        23.04.2024 13:08

                        Попробуйте в рамках вашей грамматики написать двухсвязный список

                        Я не понимаю, в чем там может быть сложность?

                        class Leaf {
                        	Leaf next;
                        	Leaf prev;	
                        };
                        
                        Leaf head;
                        Leaf tail;
                        
                        head.next = &tail;
                        head.prev = &tail;
                        
                        tail.next = &head;
                        tail.prev = &head;
                        
                        # head.*next  -> tail   # tail.*prev -> head;
                        
                        Leaf middle;
                        head.next = &middle;
                        tail.prev = &middle;
                        # head.*next  -> middle   # tail.*prev -> middle;
                        
                        middle.prev = &head;
                        middle.next = &tail;
                        # middle.*prev  -> head   # middle.*next -> tail;
                        


                      1. sdramare
                        23.04.2024 13:08

                        Если за '&'стоит счетчик ссылок, то

                        Leaf head;

                        head.next = &head;

                        циклическая ссылка и лик память.

                        Если за '&' не стоит счетчик ссылок, то

                        Leaf head;

                        Leaf tail;

                        head.next = &tail;

                        return head

                        prev удаляется и получается dangling pointer


                      1. rsashka Автор
                        23.04.2024 13:08

                        Если за '&'стоит счетчик ссылок

                        Нет, не стоит, так как это объекты одного уровня и ссылка может быть только слабой.

                        prev удаляется и получается dangling pointer

                        Вы возвращаете из функции ссылку на локальную переменную, поэтому это действительно dangling pointer. Вот только для получения данных указателя по правилам синтаксиса вам необходимое его "захватить" (оператор "звездочка"), т.е. превратить weak_ptr в shared_ptr. Поэтому тут у вас ошибка в логике, а при работе с памятью ошибки не будет.

                        Хотя такие логические ошибки легко выявлять во время анализа AST, так что скорее всего эта ошибка будет выявлена еще на этапе компиляции.


                      1. sdramare
                        23.04.2024 13:08

                        Вы возвращаете из функции ссылку на локальную переменную

                        А как вернуть структуру по значению тогда?(это к вопрос о читабельность синтаксиса).

                        Вот только для получения данных указателя по правилам синтаксиса

                        Причем тут это? У меня есть функция create_list, которая должна вернуть мне структуру Leaf. Подскажите, что мне надо писать?

                        Поэтому тут у вас ошибка в логике, а при работе с памятью ошибки не будет.

                        Давайте даже рассмотрим случай когда все ссылки

                        Leaf head = new Leaf(); //ссылка, объект в куче

                        Leaf tail = new Leaf(); //ссылка, объект в куче

                        head.next = &tail;

                        return head // или что тут у вас надо написать, *head?

                        В какой момент времени будет удален tail? Что при этом будет в поле next?


                      1. rsashka Автор
                        23.04.2024 13:08

                        return head // или что тут у вас надо написать, *head?

                        Да, этим вы возвращаете сильную ссылку, а фактически владельца объекта или std::move, если так будет проще.

                        Но в этом случае вы можете вернуть только объект head, а вот ссылка на tail протухнет, так как владельцем объекта является локальная переменная, которая к моменту возврата из функции уже будет освобождена, так как закончится её область действия.

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


                      1. sdramare
                        23.04.2024 13:08
                        +1

                        Но в этом случае вы можете вернуть только объект head, а вот ссылка на tail протухнет

                        Т.е. это будет dangling pointer, как я и написал. Следовательно, нам нужно вручную контролировать чтобы такая ситуация не произошла. Но ваша статья начинается с тезиса

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

                        Так зачем нужна ваша грамматика, если она не решает проблему, которую вы же озвучили буквально в начале вашей статьи.

                        Тут скорее всего нужно создавать какой нибудь контейнер для хранения всех листьев списка

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


                      1. rsashka Автор
                        23.04.2024 13:08

                        Раз вы не можете вернуть контейнер, значит его нужно передать снаружи, как аргумент при вызове функции. Этот объект-контейнер будет более высокого уровня, а значит сможет (по правилам синтаксиса) содержать сами объекты, а не только слабые ссылки.


                      1. sdramare
                        23.04.2024 13:08

                        Даже если это сделать, это никак не решает проблему dangling pointer. Вот у вас head.next = &tail; Положит программист этой фунции в контейнер tail или забудет и положит только head - получается как повезет. Еще хуже, что контейнер мутабельный, а значит tail можно из него удалить и опять dangling pointer. Как-будто бы вашу грамматику нужно дополнять проверками на владение, лайфтаймы и мутабельностью, но тогда получится Rust.


                      1. rsashka Автор
                        23.04.2024 13:08

                        вашу грамматику нужно дополнять проверками на владение, лайфтаймы и мутабельностью

                        Обязательно! Более того, я хочу сделать полный контроль ссылок, включая автоматическую межпотоковую синхронизацию, поэтому Rust в любом случае не получится :-)


                      1. sdramare
                        23.04.2024 13:08
                        +2

                        Как вы этого избежите, если вы не знаете в какой момент времени будет создана ссылка?

                        Вот у вас в одном потоке происходит

                        let ref_mt2 := &&ref_mt; #

                        А в другом в этот же момент ref_mt удаляется. Как вы это будете реализовывать? Как вы на уровне комплияции поймете в какой момент в другом потоке возьмут ссылку на вашу структуру? А если там будет условие, которое может никогда не отработать? В результате всеравно к счетчику придете.


                      1. rsashka Автор
                        23.04.2024 13:08

                        В результате все равно к счетчику придете.

                        В данном случае, не к счетчику, а объекту синхронизации. Точнее к счетчику, но после захвата объекта синхронизации доступа.

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


                      1. sdramare
                        23.04.2024 13:08

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

                        Вот структура

                        struct Node{ sub_node: &&Node}

                        В какой момент вы будете принимать решение о том, что ее можно удалить?


                      1. rsashka Автор
                        23.04.2024 13:08

                      1. sdramare
                        23.04.2024 13:08

                        Нет никакого простого ответа. Задача об определении времени жизни объекта на этапе комплиляции в общем случае сводится к задачае "остановки" и по-этому не решаема. А решение ее на рантайме счетчиками всегда будет порождать проблему цикличности. Запреты на копирование сильных ссылок(по сути запрет move семантики для структур) для обхода проблемы приведут к невозможности описать топологию связных графов в такой грамматике. Альтернативно счетчикам можно под капот засунуть другой вид аллокатора, например, через арены делать, но тогда не понятно зачем в принципе нужна эта грамматика, можно просто адресовать все без семантики ссылок, как в GC языках.


                      1. rsashka Автор
                        23.04.2024 13:08

                        На этапе компиляции я решаю проблему не подсчета ссылок (это действительно возможно только во время выполнения). Решается проблема циклических ссылок. И запрет копирования сильных ссылок действует только для равноправных ссылок, а move семантика тут совершенно не причем, так как она это вообще не принимает во внимание.


                      1. Mingun
                        23.04.2024 13:08
                        +1

                        Ну а в расте вы явно этот счетчик запрашиваете, оборачивая все в Arc, а если не сделаете, получаете ошибку компиляции. То есть в расте есть выбор (обернуть в Arc или подумать, как без этого обойтись), а у вас нет (сразу автоматом оборачиваете).


                      1. rsashka Автор
                        23.04.2024 13:08

                        у вас нет (сразу автоматом оборачиваете).

                        Да, все так. Только для межпотокового взаимодействия требуется не счетчик, а объект синхронизации (если мы говорит об этом). А так да, счетчик является встроенным в механизм управления ссылками и работа с с ним происходит автоматически.

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


                      1. Mingun
                        23.04.2024 13:08

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


                      1. rsashka Автор
                        23.04.2024 13:08

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


    1. rsashka Автор
      23.04.2024 13:08

      Вы правы насчет звездочек! Если их писать всегда, то фактически звездочка будет использоваться при использовании значения у каждой переменной. Наверно можно их не писать, если переменная - владелец используются как rvalue.


  1. vadimr
    23.04.2024 13:08

    Когда вам, например, операционная система передаёт указатель на свой буфер с прочитанными из файла данными, то кто в этом случае является владельцем?

    Двойную буферизацию не рассматриваем, как неэффективное решение.


    1. rsashka Автор
      23.04.2024 13:08

      Наверно не "в свой буфер", а сохраняет в тот буфер, который был её передан для сохранения прочитанных из файла данных.
      Для ОС это обычный адрес в памяти, а для языка программирования, это либо переменная-владелец буфера, либо захваченная слабая ссылка.


      1. unreal_undead2
        23.04.2024 13:08
        +1

        Я так понимаю, вопрос скорее про mmap (и дальнейшую передачу отдельных кусков в другие функции без копирования), чем read/write.


        1. rsashka Автор
          23.04.2024 13:08

          Вряд имеет смыл совмещать низкоуровневое ядерное управление памятью и прикладное программирование.


          1. unreal_undead2
            23.04.2024 13:08

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


            1. rsashka Автор
              23.04.2024 13:08

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

              Не понял, где тут требования к ресурсам и к производительности?


              1. unreal_undead2
                23.04.2024 13:08
                +1

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


                1. rsashka Автор
                  23.04.2024 13:08

                  Ну и часто ли используется mmap для чтения файлов в прикладных задачах?


                  1. unreal_undead2
                    23.04.2024 13:08
                    +1

                    В серьёзном промышленном коде, часто работающем с вводом/выводом - вполне себе используется. Даже "игрушечный" sqlite на нём живёт.


                    1. rsashka Автор
                      23.04.2024 13:08

                      sqlite отнюдь не игрушечный, да и файловый ввод/вывод для него - самое узкое место.


                      1. vadimr
                        23.04.2024 13:08

                        Дело же не в sqlite, а в принципиальном вопросе.

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

                        2. Если мы не упарываемся по производительности (т.е. в 99.9% случаев), то более чем достаточно обычного автоматического управления памятью со сборщиком мусора, и при этом не требуются никакие сильные и слабые ссылки, деструкторы и прочая хтонь.


                      1. rsashka Автор
                        23.04.2024 13:08

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


                      1. vadimr
                        23.04.2024 13:08

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


                      1. boldape
                        23.04.2024 13:08
                        +1

                        Я как немного спец по скулайту скажу что ммап для него не принципиально нужен. До реализации вол мода его вообще не было, да и с волом в режиме эксклюзив ммап не используется. В кратце там ммап только для координации работы вол между разными процессами, но НЕ для ИО.


                      1. unreal_undead2
                        23.04.2024 13:08

                        Спасибо, не знал - то есть собственно данные базы читаются/пишутся через read/write (если база в память не влезает)?


                      1. boldape
                        23.04.2024 13:08

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


                      1. unreal_undead2
                        23.04.2024 13:08

                        По сравнению с нормальным базами типа Оракла или SAP HANA - игрушка. Хотя и полезная в определённых ситуациях.


      1. vadimr
        23.04.2024 13:08

        Это двойная буферизация, не пойдёт. Тогда проще и всё остальное по значению копировать.

        Размер буфера ОС как минимум равен размеру физически читаемого блока, который прикладной программе знать неоткуда.


  1. Batalmv
    23.04.2024 13:08

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

    ОК, но по сути "мусор" надо собирать в конце каждого блока, в котором что-то было объявлено? А если я создал объект внутри функции, а потом его вернул? А он в свою очередь, имеет в своем составе другие объекты

    По сути, без описания границ видимости и времени жизни объекта ничего не понятно :(


    1. rsashka Автор
      23.04.2024 13:08

      Вы совершено правы про границы видимости!

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

      owner := 123;
      ref := & owner;
      {
        local := *ref; # В local будет сильная ссылка на owner во вложенном блоке
      }
      


      1. Batalmv
        23.04.2024 13:08

        Ну теперь вы пришли к концепции сильных/слабых ссылок в *nix файловых системах. И по сути, к тому же сборщику мусора, когда чисто сильных ссылок стало равно 0 :)

        Все равно, без точного описание границ видимости и критериев "удаления объекта" вам концепции на построить


        1. rsashka Автор
          23.04.2024 13:08

          Удаление объекта, когда число сильных ссылок стало равно 0, это не сборщик мусора, а работа обычного деструктора.


  1. YanTsys
    23.04.2024 13:08

    А как же ref1 := &val1+1;


    1. rsashka Автор
      23.04.2024 13:08
      +1

      Не ... ненужно никакой адресной арифметики.


      1. Sachavr
        23.04.2024 13:08
        +1

        "Не ... ненужно никакой адресной арифметики." - Золотые слова. Немного не в тему данного ответа, выражение зацепило, хотел сказать про низкоуровневую адресацию. По идее програмисту не надо знать про кэши, страницы и прочее аппаратное, т.к. везде разное, это дело процессора, драйверов или ОС (в крайнем случае). Программисту нужно просто выразить то что он хочет, желательно на человеческом языке. И когда я смотрю на эти закорючные символы и нечитаемую семантику в разных языках, то понимаю что будет много ошибок и непоняток. Почему нельзя написать язык где всё на человеческом языке, ведь всё равно сегодняшние закорючки нужно прогонять через компилятор. Самое сложное перевести высокий уровень в код для проца, ведь сегодняшнее железо не умеет работать с высоким уровнем и даже память ОС не может защитить аппаратно, Поэтому тяжол труд программиста в наше время. Хочется о высоком а приходится на низком. Это так, мысли вслух.


        1. rsashka Автор
          23.04.2024 13:08

          Самое сложное перевести высокий уровень в код для проца

          Это как раз самое простое. Самое сложное, это записать нужный алгоритм в семантике языка, а потом искать и исправлять ошибки.


        1. unreal_undead2
          23.04.2024 13:08
          +1

          По идее програмисту не надо знать про кэши, страницы и прочее аппаратное

          Если надо получить максимум производительности - очень даже надо.

          И когда я смотрю на эти закорючные символы

          Эффективное использование подсистемы памяти чаще выражается на уровне алгоритма, а не в каких то нюансах языка.


  1. Mingun
    23.04.2024 13:08
    +1

    ref1 = 3; # Ошибка - переменная является ссылкой!
    ...
    ref1 = 5;
    # val1 = 5, а val2 = 5
    

    А вы говорите, все наглядно-очевидно... (не спрашиваю о том, как в val1 и val2 оказалось одинаковое значение... когда от val1 уже все ссылки отцепились). Если уж нужна очевидность, то пусть ссылки всегда носят с собой дополнительный символ, говорящий -- "я ссылка". А доступ к данным -- это убирание этого символа. Это и символично -- убираем уровень косвенности -- получаем данные. Хотя тогда могут возникнуть проблемы с обобщенным кодом -- даже если он не зависит от того, ссылка там или объект, синтаксис чисто формально может требоваться разный.


    1. rsashka Автор
      23.04.2024 13:08

      Спасибо! Это действительно косяк в примере. Тут должно быть по другому:

      ...
      
      
      ref1 = ref2; # ОК - одна ссылка присваивается другой ссылке
      # ref1 -> val2, а ref2 -> val2
      
      *ref1 = 5;
      # *ref1 -> 5,  *ref2 -> 5
      


  1. ARad
    23.04.2024 13:08

    ref1 = 3; # Ошибка - переменная является ссылкой!

    А ниже, последняя строка

    ref1 = 5;

    Вы описались? Или я что то не понимаю?


    1. rsashka Автор
      23.04.2024 13:08

      Описался


  1. 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()


    1. unreal_undead2
      23.04.2024 13:08

      Слишком сложно, в старом Фортране значение литерала менялось легче )


  1. rukhi7
    23.04.2024 13:08
    +1

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

    Отличается способ копирования переменной:

    либо мы создаем еще один описатель этой области памяти, то есть переменной. Описатель, который обязательно содержит адрес этой области памяти,

    либо мы копируем содержимое этой области памяти в область памяти с такими же параметрами.

    Любая попытка абстрагироваться от того что программы работают с памятью и с ее адресами это очередная попытка всех запутать, мне кажется. Это как в электрике пытаться абстрагироваться от закона Ома, мне кажется. Типа я вот буду провода соединять супер клеем и лампочка у меня загорится более безопасно.


    1. rsashka Автор
      23.04.2024 13:08
      +1

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

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


      1. rukhi7
        23.04.2024 13:08

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

        но аналогию можно продолжить конечно:

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

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


      1. rukhi7
        23.04.2024 13:08

        вот если вы будете код портировать с Х86 платформы на ARM платформу, получится у вас от абстрагироваться от машинных инструкций?

        Это же в общем то вопрос области разработки в том числе, или вы не согласны?


        1. rsashka Автор
          23.04.2024 13:08

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

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

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

          Поэтому мне очень нравится подход Python и Java (любой объект - это ссылка на область в памяти), очень удобно не заботится о распределении памяти, но мне очень не нравится плата за это - необходимость использования сборщиков мусора и лишние накладные расходы. Мне нравится подход С++ за его минимализм оверхеда, но все убивает необходимость ручного управления памятью, в которой очень легко ошибиться.

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


          1. rukhi7
            23.04.2024 13:08

            очень нравится подход Python и Java (любой объект - это ссылка на область в памяти),

            ...

            Мне нравится подход С++ за его минимализм оверхеда, но все убивает необходимость ручного управления памятью, в которой очень легко ошибиться.

            Я на С# пишу много теперь, сборщик мусора там вроде как не является необходимостью, никто не мешает удалить объект вручную, насколько я знаю на Java также можно действовать.

            Главная проблема С++ по моему хидера и ручное описание интерфейсов-отсутствие полной рефлексии по типам. Для управления памятью все уже придумано например COM-объекты. Проблема только с оформлением через те же хидера, по моему. Ну и однажды применив технику надо не лениться ее следовать последовательно, а не так что тут у нас было так, а потом как попало.


        1. unreal_undead2
          23.04.2024 13:08

          Для большей части прикладного кода компилятор всё абстрагирует - проблемы будут скорее не с ISA, а со слабой моделью памяти, что решается высокоуровневыми средствами. Но, скажем, спортировать на новую архитектуру Javascript движок - это уже задачка интересная )


          1. rukhi7
            23.04.2024 13:08

            спортировать на новую архитектуру Javascript движок - это уже задачка интересная 

            эта задача довольна абстрактная, так как какого типа, объема, производительности, ... скрипты вы на этом движке собираетесь запускать - большой вопрос.

            А вот если вы будете портировать какой-то Mpeg аудио декодер, или не дай бог видео декодер, тут уже достаточно хорошо известны требования которые происходят из параметров сигнала - звука/картинки который надо воспроизвести. И вряд ли получится обойтись без знания аппаратных особенностей платформы и исходной и целевой для портирования.


            1. unreal_undead2
              23.04.2024 13:08

              так как какого типа, объема, производительности, ... скрипты вы на этом движке собираетесь запускать - большой вопрос

              Да просто броузиться, чтобы всяческие Google Docs/Maps адекватно работали.

              какой-то Mpeg аудио декодер, или не дай бог видео декодер

              Согласен, там нужно как минимум доступные SIMD инструкции/регистры запользовать. Вроде есть переносимые подходы (скажем, Google Highway) - но не знаю, насколько эффективный код они генерируют.


              1. rukhi7
                23.04.2024 13:08

                Вроде есть переносимые подходы (скажем, Google Highway) - но не знаю, насколько эффективный код они генерируют.

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

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

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


                1. unreal_undead2
                  23.04.2024 13:08

                  они генерируют код для каких-то общих случаев

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

                  Эффективность локального браузера частично можно обеспечить за счет локальных-же специфичных для данной платформы модулей

                  Но всё таки для сайтов с нетривиальными скриптами главное - эффективная JIT компиляция JS кода, при этом llvm там не особо применим, так что backend для новой архитектуры придётся писать руками.


                  1. rukhi7
                    23.04.2024 13:08

                    только руками на ассемблере или интринсиках, получается Highway где то посередине, только вот насколько она широкая...

                    вот я руками на ассемблере и делал, максимум что можно потом сделать с этим специальным решением на ассемблере, написать программу которая будет генерировать этот ассемблер для этого КОНКРЕТНОГО алгоритма, как-то параметризованного для более менее похожих наборов инструкций процессора и некоторых параметров алгоритма.

                    То есть КАЖДЫЙ новый алгоритм требует такой специальной работы это нельзя автоматизировать пока все возможные алгоритмы для всех принципиально разных платформ не будут написаны и сведены в какую-то общую базу алгоритмов.


  1. 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

    ИМХО, пример того, когда сделали чисто для языковой симметрии в ущерб читаемости и практической применимости. Я считаю, что указатель и ссылка, при всей внутренней схожести, на уровне языкового дизайна все-же разные конструкции: указатель - мощный низкоуровневый инструмент для построения явных связей на уровне адресов; ссылка - высокоуровневая абстракция, предназначенная как раз для того чтобы уйти от адресов, явных размыменований и прочего.


    1. rsashka Автор
      23.04.2024 13:08

      Большое спасибо за очередной развернутый комментарий от вас!

      По вашей терминологии предлагается "среднеуровневый" подход в управлении ссылками, т.е. все хранится как объект - "по значению или по ссылке, мы абстрагируемся от способа хранения и работаем просто с объектами.", но с возможностью явного использования данной ссылки в низкоуровневых языках (С/С++) напрямую без каких либо оберток, надстроек или конвертации.

      Плюс унификация синтаксиса при работе со ссылками (а чтобы не было все в звездочках, разрешить их не писать, если переменная - владелец используются как rvalue), т.е. самый типичный случай использования.

      К тому в эту канву очень удачно будет вписываться вообще полный контроль за ссылками, в том числе и за межпоточным взаимодействием и все это опять же во время компиляции, а такого нет даже в Rust :-)


    1. posledam
      23.04.2024 13:08

      В C# не "всё есть объект". Есть вполне себе значимые типы (структуры) и ссылочные типы (классы). Значимые типы передаются по значению, ссылочные по ссылке (указателю). При этом можно иметь ссылку на значение или указатель. И всё это разнообразие доступно, понятно и работает без звёздочек, амперсандов и стрелочек типа ->.

      Какой смысл возвращаться к специальному ссылочно-значимому синтаксису, совершенно непонятно.


      1. rsashka Автор
        23.04.2024 13:08

        При работе с сылками в С# вы на уровне синтаксиса не можете отличить аргумент по ссылке от аргумента по значению.


        1. posledam
          23.04.2024 13:08
          +2

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


          1. rsashka Автор
            23.04.2024 13:08

            Обращение к локальной переменной легко отличается от доступа к полю (свойству) на уровне синтаксиса.


            1. NeoCode
              23.04.2024 13:08

              Не отличается: вот пример, обращение к локальной переменной (аргументу функции) и к полю соврешенно одинаковое (хотя мы знаем что на низком уровне в первом случае работа через указатель this, во втором - просто доступ к стеку). Чтобы отличить, нужно смотреть место объявления. А это может быть где-то далеко в коде (хорошо современные IDE выводят подсказки по наведению мыши на переменную).

              class Foo {
                int x;
                void bar(int y) {
                  x = 10;
                  y = 10;
                }
              }


              1. rsashka Автор
                23.04.2024 13:08

                Я конечно не спец по С#, но такой синтаксис, который мешает в одну кучу поля класса, локальные переменными и ссылки мне однозначно не нравится.


                1. mmatvey1123
                  23.04.2024 13:08
                  +1

                  Вроде во многих языках можно так обращаться к полям класса. Та же java, c++.

                  Мне так же не нравится данный синтаксис. Именно по этому есть ключевое слово this, явно показывающее, что происходит обращение к полю класса.


                  1. rsashka Автор
                    23.04.2024 13:08

                    Либо через точку, если идет обращение к полям структуры.


      1. nv13
        23.04.2024 13:08
        +1

        И всё это разнообразие доступно, понятно и работает без звёздочек, амперсандов и стрелочек типа ->

        А мне не нравится. Раньше с этими загогулинами понятно всегда было где ты и чем тебе это грозит, а теперь можно так а можно по новому. Слишком уж диверситифицировано)


  1. boldape
    23.04.2024 13:08
    +1

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

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

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

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

    Кстати Валя куда идейно более проработана чем аргентумом и тем более ваша идея. Так что я вашу идею не куплю.