Войти
ПрограммированиеФорумОбщее

Типизированные int'ы

Страницы: 1 2 38 9 Следующая »
#0
(Правка: 17:18) 15:29, 13 июля 2019

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

std::vector<Asteroid> asteroids;
и, разумеется, тип индекса для обращения к элементу такого массива — это size_t:
size_t asteroidIndex = 42;
ProcessAsteroid(asteroids[asteroidIndex]);
Представим далее, что гипотетически, что мы хотим, например, хранить массив космических корабликов, у каждого из которых есть своя цель-астероид.
struct Ship
{
  size_t asteroidIndex;
};
std::vector<Ship> ships;
и далее что-то проделать с астероидом, на который нацелен 42 кораблик:
  size_t shipIndex = 42;
  DoSomething(asteroids[ships[shipIndex].asteroidIndex]);

Совершенно типичный код, с которым часто приходится иметь дело в том или ином виде. Особенность, на которую я бы хотел обратить внимание — что в таком коде каждый массив индексируется одним и тем же типом size_t и вот такой код прекрасно скомпилируется:

  size_t shipIndex = 42;
  DoSomething(asteroids[shipIndex]);
хотя по сути он представляет из себя нонсенс, так как shipIndex — это номер кораблика, а мы обращаемся по этому номеру к массиву астероидов. То есть получается так, что номер кораблика и номер астероида — это, вообще говоря, несовместимые типы, которые никогда не должны быть взаимозаменяемы. Если всё ещё кажется, что такую ошибку допустить нельзя, то вот ещё пример:
float totalDamage = GetTotalDamage(asteroidIndex, shipIndex);
Вроде бы в коде всё хорошо, он компилируется, только выдаёт полную ерунду или падает. Причина — в прототипе функции аргументы объявлены в другом порядке:
float GetTotalDamage(int shipIndex, int asteroidIndex);


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

using ShipIndex = size_t;
using AsteroidIndex = size_t;
struct Ship
{
  AsteroidIndex asteroidIndex;
};
Однако, в таком подходе типы на этапе компиляции всё равно оказываются взаимозаменяемы. В тензорной библиотеке, которую я писал, я все массивы и все индексы сделал типизированные. Примерно так:
struct Ship
{
  Index<Ship> asteroidIndex; //Index — шаблонный класс, обёртка вокруг size_t, которая только лишь препятствует присваиванию индексов разных типов
};
almost::vector<Ship> ships; //almost::vector работает точно так же, как std::vector, но индексация происходит по Index<T>
almost::vector<Asteroid> asteroids;
for(auto ship : ships)
{
  //auto test1 = ships[ship.asteroidIndex]; //не скомплится, потому что в массив ships можно обращаться только по Index<Ship>, а не по Index<Asteroid>
  auto test2 = asteroids[ship.asteroidIndex]; //ок
}
auto test3 = asteroids[AsteroidIndex(0)]; //поддерживается явный каст, который используется, например, при сериализации и отладке
В тензорной библиотеке я пошёл дальше и завёл свой тип индексов не только для массивов, но и для матриц, например. Если все индексы разных типов, то не возникает вопросов, в каком порядке нужно умножать матрицы — слева или справа, нужно ли матрицу транспонировать перед умножением на вектор итп, потому что неправильный код просто не скомпилится.

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

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

Вопросов у меня несколько. Какие подводные камни? Почему система не распространилась до того, как я её (пере?)изобрёл? Какие существуют реализации этой идеи? Как реализовать эту идею в более слабых языках вроде glsl?

#1
15:50, 13 июля 2019

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

> Почему система не распространилась до того, как я её (пере?)изобрёл?
Слишком много геммора с сомнительным выигрышем.

#2
15:54, 13 июля 2019

std::vector<Asteroid>::iterator разве это не то что ты изобретаешь? типизация есть, нельзя обратиться в вектор с другим содержимым - есть.. или речь об raw массивах типа матриц?

#3
(Правка: 16:03) 16:03, 13 июля 2019

Oxyd
> std::vector<Asteroid>::iterator
никто в своём уме не будет хранить массив итераторов на что-то. к тому же итераторы инвалидируются после модификации вектора, их нельзя сериализовать и они хранят в себе указатель на вектор. короче, итераторы для векторов в целях нульпоста — это слишком толстый синтаксический оверхед ни из-за чего.

#4
(Правка: 16:07) 16:04, 13 июля 2019

Suslik
а индексы не инвалидируются совсем ага

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

насчет неиспользования итераторов это вообще дичь

#5
16:04, 13 июля 2019

Suslik
> Почему система не распространилась до того, как я её (пере?)изобрёл?
Какая в итоге решается проблема - защита от криворукости? Ответ в вопросе - нужна ли такая защита в обмен на усложнение кода?

#6
16:14, 13 июля 2019

Index-based model + Aggregation-based model

#7
(Правка: 16:18) 16:14, 13 июля 2019

Suslik
> Какие существуют реализации этой идеи?
кстати, в том же vulkan.hpp используется такая система для всех хендлов. например, если возвращется хендл на vk::DescriptorSet, то его нельзя случайно присвоить хендлу на vk::DescriptorSetLayout, потому что они разных типов, хотя оба являются обычными интами. когда я пользуюсь такими вещами, единственный вопрос, который у меня возникает — почему так не делается везде? почему поддержки индексов разных типов нет на уровне языка или хотя бы стандартной библиотеки? почему для вулкана эти хендлы генерятся ужасным кодогенератором, когда это могло бы быть стандартной фичей языка?

BingoBongo
> Ответ в вопросе - нужна ли такая защита в обмен на усложнение кода?
а нужны ли типизированные указатели Object* вместо void*? практика показывает, что да. в том же OpenGL все хендлы — это нетипизированный GLuint и совершенно нормальным считается 80% времени работы с этим прекрасным API тратить на отладку, когда GL_INVALID_OPERATION вываливается, так как image unit был передан вместо texture id или texture id был передан вместо texture block или texture block вместо sampler index, хотя всех этих проблем вообще бы не существовало, если бы они использовали типизированные индексы/хендлы как в том же вулкане или d3d.

#8
(Правка: 16:32) 16:31, 13 июля 2019

Suslik
> то его нельзя случайно присвоить хендлу на vk::DescriptorSetLayout, потому что
> они разных типов, хотя оба являются обычными интами
???
(int)(void)(int) ?? (образно говоря (тип)(воид)(тип))
я лично так делал в томже вулкане для передаче однотипных констант разным функциям которые требовали техже констант но переименованных

ох вейт у вас тут C++, я про Си говорил

#9
16:36, 13 июля 2019

Ещё немного гуглежа показали, что это, к счастью, уже частично изобретено и называется strong typedefs: https://arne-mertz.de/2016/11/stronger-types/ и, к сожалению, это не реализовано в базовом языке.

однако, для этого даже существуют реализованные кем-то библиотеки! https://github.com/joboccara/NamedType

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

#10
16:41, 13 июля 2019

Suslik
> суть в том, что здесь не только для интов используются уникальные типы, а
> вообще для всего. и это правильно, потому что не должно быть в двиге
> возможности присвоить строки с именем персонажа какую-нибудь строку с
> http-запросом, это должны быть разные типы.
звучит как изобретение Java

#11
16:44, 13 июля 2019

Suslik
> а нужны ли типизированные указатели Object* вместо void*? практика показывает,
> что да.
Как еще получать список полей и методов после -> :) Указатели очень часто встречаются и увидев у класса в private: 20 void* -ов непонятно чего можно подофигеть. В то время как 20 аналогичных int -ов, которые отвечают за абсолютно разные вещи ты не увидишь, т.к. int в большинстве случаев - это просто параметр. int - индекс в каком-то специфичном массиве редок.

Также int - это всегда число. И ты и компилятор это понимаете на месте. Храня в int'е индекс массива, ты наделяешь данный int дополнительным свойством. А нетипизированный указатель - это вообще ни что, он ничего не значит и может быть чем угодно. Нам нужны не столько указатели в качестве адресов памяти, сколько предназначение этой самой памяти. Поэтому сравнение с указателями некорректно.

#12
(Правка: 16:52) 16:50, 13 июля 2019

На ATS уже сейчас вполне реализуема.
Для параноиков, можно даже запретить использовать индекс астероида из других массивов астероидов.

BingoBongo
> Храня в int'е индекс массива, ты наделяешь данный int дополнительным свойством.
Это далеко не вся возможная наркомания :)
Например, в типе можно указать диапазон значений, типа {i:int | 0 < i < 10} и т.д. (см. зависимые типы)

#13
(Правка: 16:58) 16:53, 13 июля 2019

ещё немного бальзама на душу — идею, к счастью, изобрели до меня и она даже пользуется определённой популярностью: http://nullptr.nl/2018/02/phantom-types/

и о её стандартизации, как выяснилось, уже давно ходят разговоры: https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/Y-7cdQcNDrk но не реализовали её в первую очередь потому что:

The problem with strong typedefs has never been the question of "what syntax do I use?" It's always been a matter of what behavior you get in the myriad of circumstances where you want strong typedef. That is, the question of how strong of an alias do you want.

`enum class` works as a strong typedef for integral types because there's pretty much only one level of "strength" as far as integers are concerned. User-defined types have many possible levels of "strength", and different people have different needs.

Zegalur
> На ATS уже сейчас вполне реализуема.
вундервафля. хочу в c++.


BingoBongo
> Указатели очень часто встречаются и увидев у класса в private: 20 void* -ов
> непонятно чего можно подофигеть.
так в том-то и дело, что 20 интов — это ничуть не лучше, чем 20 void*, однако почему-то использовать 20 интов люди привыкли и ничего, а void* для них почему-то не нравится, хотя смысл-то один и тот же.

#14
17:05, 13 июля 2019

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

int number = 42;
float fnumber = number;

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

Suslik
> возможности присвоить строки с именем персонажа какую-нибудь строку с
> http-запросом, это должны быть разные типы.

Ага, а при этом какие-то странные люди изобрели полиморфизм и возможность отдавать разные типы в один запрос.

Suslik
> DoSomething(asteroids[shipIndex]);
> хотя по сути он представляет из себя нонсенс, так как shipIndex — это номер
> кораблика, а мы обращаемся по этому номеру к массиву астероидов.

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

Страницы: 1 2 38 9 Следующая »
ПрограммированиеФорумОбщее