среда, 11 июня 2008 г.

Инстанцирование шаблонов смещениями полей.

Надо сказать, что с шаблонами я как-то не особо дружу, но надо когда-то начинать.

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

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

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

Шаблоны можно инстанцировать именами типов, или константами. Причем тип у константы может быть любой, главное чтобы ее значение было заранее известно и неизменно. Именно так и происходит с предварительно описанным классом, которым мы инстанцируем шаблон. Поскольку типы объектов заранее не известны, то и List и Link - шаблонные. Здесь я приведу упрощенную модель.
template <typename I>
class Link {
...
I *next;
...
};

template<typename I, Link<I> I::* L>
class List {
...
I *m_first;
...
};

Здесь самое интересное - это второй параметр. Фактически это смещение поля, которое надо использовать с помощью операторов ->* или .* .
class List {
...
void Insert (I *i)
{
(i->*L).next = m_first;
m_first = i;
}
...
}

Инстанциирование осуществляется следующим образом:

class Item {
Link<Item> m_link;
Link<Item> m_link2;
};

List<Item, &Item::m_link> list1;
List<Item, &Item::m_link2> list2;

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

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

PS: В принципе тип Link<I> I::* достаточно интересен. ostream принимает его наверное за bool, и на любые, даже нулевые смещения выводит 1. А привести его к чему-то у меня не получилось вообще. Но старый, добрый printf с помощью %p отобразил реальное значение этого типа, которое как и ожидалось является смещением в классе.

PPS: Полем я как-то по старинке называю переменные-члены.

9 коммент.:

Анонимный комментирует...

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

Анонимный комментирует...

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

Андрей Валяев комментирует...

Отслеживание жизни объекта лежит за рамками этого поста :)

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

Всеравно в ядре все немного сложнее. Не получится вот так просто взять и сказать delete thread; В любом случае нужно многое проделать предварительно в зависимости от состояния нити.

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

Вообще здесь все как сделаешь. Вариантов масса.

Анонимный комментирует...

Хочется немного добавить.О типе Link<I> I::*.

Указатель на мембер по стандарту не может быть приведен ни к какому типу, кроме bool. Это его свойство используется для создания safe bool. Как устроен этот указатель программно, никто не гарантирует. То что смещение численно равно нулю, не означает, что указатель приведется к false. По стандарту указатель будет приведен к false, толькое если ему было присвоено значение 0 (это совсем не означает, что на битовом уровне указатель обязан равнятся 0 - может, к примеру, 0x12345).

Это и понятно. Представим такой код.

struct C
{
int c;
};

int main()
{
int C::*p1 = &C::c;
int C::*p2 = 0;

std::cout << (bool)p1 << " " << (bool)p2;

return 0;
}

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


PS. Шаблоны можно инстанциировать ещё и неинстанциированными шаблонами.

Анонимный комментирует...

По идее, и float не должен ни к чему приводится, ведь на битовом уровне 0.0 совсем не 00000000.

Анонимный комментирует...

Как раз 0 во float-е по стандарту IEEE 754 кодируется всеми нулями.


Zero is a special value denoted with an exponent field of zero and a fraction field of zero.


Для указателя на мембер так сделано, потому что не понятно что хотел автор приводя такой указатель, скажем, к int-у. Ведь, в теории, программист не должен знать, как именно обрабатываются такие указатели. Вот, к примеру, в майкрософтовском компиляторе указатель на мембер функцию состоит из 8 байт (в 32 битной версии, конечно). 4 байта на указатель на функцию, 2 байта - смещение, 2 байта - номер в таблице виртуальных методов. Так что он к инту даже и не приводится.

Андрей Валяев комментирует...

Надо сказать что если float и имеет размер 32 бита, но double и еще один тип, не имеющия названия в c/c++ имеют размер 8 и 10 байт соответственно.

И всеравно их можно привести к int, ибо даже в сопроцессоре есть инструкция fist[p], которая возвращает целую часть числа.

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

И стоит отметить что подобное приведение указателей всегда считалось плохим делом.

Логических типов как таковых в си не было вообще, это только в C++ появилась такая сущность и стало считаться правильным логические сравнения проводить неявно, а все остальные явно. Всмысле что

bool success;
void *ptr;

if (succes) // хорошо
if (!ptr) // плохо
if (ptr == 0) // хорошо

Вообще приведения по умолчанию чаще вредят.

Андрей Валяев комментирует...

Забыл сказать спасибо asmal, за содержательные комментарии. много нового узнал.

Хотя политика приведения непонятна. почему к bool? и какое значение будет соответствовать false?

Анонимный комментирует...

Политика приведения проста. Если указатель указывает на какой-то мембер - значит он приводится к true, в противном случае - к false.

struct C
{
int c;
};

int main()
{
int C::*p1 = &C::c;
int C::*p2 = 0;
}

Тут bool(p1) == true, a bool(p2) == false.

Это как с обычными указателями. Указывает - true, не указывает - false. Просто обычные указатели ещё имеют арифметику, а для мембер-указателей (как и для указателей на обычные функции) она бессмысленна. Приведение к чему-то кроме bool тоже бессмыслено.

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

Очень хорошо про это написано здесь: здесь.