в чем отличие между реальными и виртуальными таблицами
Что такое таблица виртуальных таблиц?
Однажды в Slack я наткнулся на новый акроним для моего глоссария акронимов C++: “VTT.” Godbolt:
“VTT” в данном контексте означает «таблица виртуальных таблиц» (virtual table table). Это вспомогательная структура данных, используемая (в Itanium C++ ABI) при создании некоторых базовых классов, которые сами унаследованы от виртуальных базовых классов. VTT следуют тем же правилам размещения, что и виртуальные таблицы (vtable) и информация о типе (typeinfo), так что если вы получили ошибку, приведённую выше, вы можете просто мысленно подставить «vtable» вместо «VTT», и начать отладку. (Скорее всего, вы оставили неопределённой ключевую функцию класса). Для того, чтобы увидеть, почему VTT, или аналогичная структура, необходима, начнём с основ.
Порядок конструирования для невиртуального наследования
Когда у нас есть иерархия наследования, базовые классы конструируются, начиная с наиболее базового. Чтобы сконструировать Charlie, мы должны сначала сконструировать его родительские классы MrsBucket и MrBucket, рекурсивно, чтобы сконструировать MrBucket, мы должны сначала сконструировать его родительские классы GrandmaJosephine и GrandpaJoe.
Порядок конструирования для виртуальных базовых классов
Но виртуальное наследование путает все карты! При виртуальном наследовании, мы можем иметь ромбовидную иерархию, в которой два различных родительских класса могут иметь общего предка.
В прошлом разделе, каждый конструктор отвечал за вызов конструктора своего базового класса. Но сейчас у нас виртуальное наследование, и конструкторы M и F должны как-то знать, что не нужно конструировать G, потому что он общий. Если бы M и F были ответственные за конструирование базовых объектов в этом случае, общий базовый объект был бы сконструирован дважды, а это не очень хорошо.
Чтобы работать с подобъектами виртуального наследования, Itanium C++ ABI разделяет каждый конструктор на две части: конструктор базового объекта и и конструктор полного объекта. Конструктор базового объекта отвечает за конструирование всех подобъектов невиртуального наследования (и их подобъектов, и установку их vptr на их vtable, и запуск кода в фигурных скобках в коде C++). Конструктор полного объекта, который вызывается каждый раз, когда вы создаёте полный объект C++, отвечает за конструирование всех подобъектов виртуального наследования производного объекта и затем делает всё остальное.
Рассмотрим разницу между нашим примером A B C D E из предыдущего раздела и следующим примером:
Конструктор полного объекта Е сначала вызывает конструкторы базового объекта виртуальных подобъектов A и C; затем вызываются конструкторы базового объекта невиртуального наследования B и D. B и D не несут больше ответственность за конструирование A и C соответственно.
Конструирование таблиц vtable
Пусть у нас есть класс с какими-нибудь виртуальными методами, например, такой (Godbolt):
Когда мы конструируем Lion, мы начинаем с конструирования базового подобъекта Cat. Конструктор Cat вызывает poke(). В этой точке мы имеем только один объект Cat — мы пока не инициализировали данные-члены, которые необходимы, чтобы сделать объект Lion. Если конструктор Cat вызовет Lion::poke(), он может попытаться изменить неинициализированный член std::string roar и мы получим UB. Итак, стандарт С++ обязывает нас сделать это в конструкторе Cat, вызов виртуального метода poke() должен вызвать Cat::poke(), не Lion::poke()!
В этом нет проблемы. Компилятор просто заставляет Cat::Cat() (и версию для базового объекта, и версию для полного объекта) начинаться с установки vptr объекта на таблицу vtable объекта Cat. Lion::Lion() вызовет Cat::Cat(), и затем сбросит vptr в значение указателя на таблицу vtable для объекта Cat внутри Lion, перед тем, как запустить код в круглых скобках. Без проблем!
Смещения при виртуальном наследовании
Пусть Cat виртуально наследуется от Animal. Тогда таблица vtable для Cat хранит не только указатели на функции для виртуальных функций-членов Cat, но также смещение виртуального подобъекта Animal внутри Cat. (Godbolt.)
Конструктор Cat запрашивает член Animal::data. Если этот объект Cat является базовым подобъектом объекта Nermal, то его данные-члены находятся по смещению 8, прямо за vptr. Но если объект Cat является базовым подобъектом объекта Garfield, то данные-члены находятся по смещению 16, за vptr и Garfield::padding. Чтобы справиться с ситуацией, Itanium ABI сохраняет смещения виртуальных базовые объектов в таблице vtable объекта Cat. Таблица vtable для Cat-в-Nermal сохраняет тот факт, что Animal, базовый подобъект Cat, сохранён по смещению 8; таблица vtable для Cat-в-Garfield сохраняет тот факт, что Animal, базовый подобъект Cat, сохранён по смещению 16.
Сейчас объединим это с предыдущим разделом. Компилятор должен убедиться, что Cat::Cat() (как версия базового объекта, так и версия полного объекта) начинается с установки vptr на таблицу vtable для Cat-в-Nermal или на vtable для Cat-в-Garfield, в зависимости от типа наиболее производного объекта! Но как это работает?
Конструктор полного объекта для наиболее производного объекта должен заранее вычислить, на какую таблицу vtable он хочет, чтобы ссылался vptr базового подобъекта в течение времени конструирования объекта, и затем конструктор полного объекта для наиболее производного объекта должен передать эту информацию вниз, в конструктор базового объекта базового подобъекта как скрытый параметр! Посмотрим на сгенерированный код для Cat::Cat() (Godbolt):
Конструктор базового объекта принимает не только этот скрытый параметр в %rdi, но также скрытый параметр VTT в %rsi! Конструктор базового объекта загружает адрес из (%rsi) и сохраняет адрес в таблице vtable объекта Cat.
Кто бы ни вызвал конструктор базового объекта Cat, он ответственен за предвычисление того, какой адрес Cat::Cat() должен быть записан в vptr и за установку указателя в (%rsi) на этот адрес.
Зачем нужен ещё один уровень индиректности?
Рассмотрим конструктор полного объекта Nermal.
Почему _ZTC6Nermal0_3Cat+24 размещено в секции data и его адрес передаётся в %rsi, вместо того, чтобы просто передать _ZTC6Nermal0_3Cat+24 напрямую?
Так происходит потому, что у нас может быть несколько уровней наследования! На каждом уровне наследования конструктор базового объекта должен установить vptr и затем, возможно, передать управление дальше по цепочке, к следующему базовому конструктору, который может установить vptrs в какое-то другое значение. Это подразумевает список либо таблицу указателей на vtable.
Вот конкретный пример (Godbolt):
Объект базового конструктора Grandparent должен установить свой vptr в значение Grandparent-в-чём-нибудь, что является наиболее производным классом. Конструктор базового объекта Parent должен сначала вызвать Grandparent::Grandparent() с подходящим %rsi, и затем установить vptr в значение Parent-в-чём-нибудь, что является наиболее производным классом. Способ реализации этого для Gretel:
Вы можете видеть в Godbolt, что конструктор базового объекта класса Parent сначала вызывает Grandparent::Grandparent() с %rsi+8, затем устанавливает собственный vptr в (%rsi). Итак, здесь используется факт того, что Гретель, так сказать, тщательно проложила тропу из хлебных крошек, по которой шли все её базовые классы при конструировании.
Та же VTT используется в деструкторе (Godbolt). Насколько я знаю, нулевая строка таблицы VTT никогда не используется. Конструктор Gretel загружает vtable для Gretel+24 в vptr, но он знает, что этот адрес статический, его никогда не нужно загружать из VTT. Я думаю, что нулевая строка таблицы сохранилась просто по историческим причинам. (И конечно, компилятор не может просто выбросить её, потому что это будет нарушением Itanium ABI и станет невозможной линковка со старым кодом, который придерживается Itanium-ABI).
Вот и всё, мы рассмотрели таблицу виртуальных таблиц, или VTT.
Дальнейшая информация
Вы можете найти информацию о VTT в этих местах:
Наконец, я должен повторить, что VTT, это особенность Itanium C++ ABI, и используется в Linux, OSX, и т.п. MSVC ABI, используемое в Windows, не имеет VTT, и использует совершенно иной механизм для виртуального наследования. Я (пока что) практически ничего не знаю о MSVC ABI, но может быть, однажды, я всё выясню и напишу об этом пост!
Конструктор запросов 1С — обучение на примерах
Урок 6. Виртуальные таблицы и их использование в конструкторе запросов
Задача 1: Получить остатки номенклатуры на указанном складе на конец месяца.
Задача 2: Получить актуальную цену на конец месяца по указанной номенклатуре и типу цен.
Новые механизмы: заполнение параметров виртуальных таблиц.
Теоретическая часть урока №6
У некоторых объектов метаданных помимо основной таблицы в базе данных присутствуют виртуальные таблицы. Они облегчают доступ к некоторой информации содержащейся в основной таблице. Использовать данные виртуальных таблиц можно при помощи запросов, либо при помощи специальных методов встроенного языка 1с 8.
Рассмотрим основные виды виртуальных таблиц 1с для различных объектов метаданных:
Конструктор запросов позволяет работать с виртуальными таблицами регистров. Если у регистра есть виртуальные таблицы, то они будут находится в разделе База данных на вкладке Таблицы и поля после основной таблицы регистра.
Для того чтобы получить нужные данные из виртуальной таблицы регистра, необходимо заполнить ее параметры. Разберем заполнение параметров для основных виртуальных таблиц. Для того чтобы открыть окно параметров виртуальной таблицы, ее необходимо перенести из раздела База данных, в раздел Таблицы, выделить и нажать в кнопку Параметры виртуальной таблицы.
СрезПоследних и СрезПервых
Остатки в регистре накопления
Остатки в регистре бухгалтерии
При помощи данного параметра можно увеличить скорость выполнения запроса, исключив ненужную аналитику. Субконто в виртуальной таблице будут доступны в том порядке, в котором они стоят в массиве. Если в параметр передано меньше видов субконто, чем существует на счете, то не указанные в параметре субконто использовать нельзя. Данный параметр не является обязательным, если он не задан, то в виртуальной таблице используются все доступные субконто.
Обороты в регистре накопления
Обороты в регистре бухгалтерии
Остатки и обороты в регистре накопления
Остатки и обороты в регистре бухгалтерии
Все параметры используемые в данной таблице были описаны в предыдущих пунктах.
Обороты Дебет Кредит
Движения с субконто
Практическая часть урока №6
В данном разделе нам предстоит решить две задачи по пройденной теме.
Задача 1
Получить остатки номенклатуры на указанном складе на конец месяца.
Для простоты предположим, что весь учет товаров на складах идет по 41 счету бухгалтерского учета.
В итоге у нас получится запрос со следующим текстом:
Задача 2
Получить актуальную цену на конец месяца по указанной номенклатуре и типу цен.
В итоге у нас получится запрос со следующим текстом:
Регистры накопления. Виртуальные таблицы. Часть №1: Обороты
О регистрах накопления
В нескольких статьях представлены основные сведения о внутреннем устройстве регистров накопления, о SQL-запросах платформы при работе с ними и их изменение в зависимости от настроек регистра. Подробно описана работа платформы с разными типами регистров (остатков и накопления), а также принцип действия агрегатов.
Материалы созданы во времена платформы 8.2, поэтому некоторые моменты могут быть уже не актуальными, но основные принципы работы остались неизменными.
Больше года назад сайт был закрыт. Некоторые из его материалов будут реанимированы на Инфостарт.
Конкретно в этой статье речь идет о виртуальной таблице «Обороты» регистров накопления в базе данных. Все примеры из публикации Вы можете найти на GitHub.
Виртуальные таблицы
В статье «Регистры накопления. Структура хранения в базе данных» мы рассматривали таблицы, которые использует платформа для хранения движений в регистрах накопления, а также его итоговых оборотов или остатков в зависимости от вида регистра («Остатки» или «Обороты»). Также были подробно рассмотрены действия платформы с таблицами остатков и оборотов при записи движений в регистр.
Сегодня в статье проанализируем SQL-запросы, формируемые платформой, при обращении к виртуальным таблицам регистра. Напомню, что у регистров накопления существует всего три виртуальных таблицы:
Как мы видим, кроме физической таблицы движений, для которой в базе данных создается отдельная таблица, также имеются виртуальные таблицы. Всего их три:
Последние две становятся доступными только если вид регистра установлен как «Остатки».
Главное отличие виртуальных таблиц от физических: виртуальные таблицы не хранятся непосредственно в базе данных. При выборке данных из виртуальной таблицы платформа формирует некоторый запрос в зависимости от переданных параметров, который может получать записи из двух и более таблиц для формирования конечного результата.
Далее в статье проанализируем SQL-запросы платформы 1С:Предприятие 8.2 при обращении к виртуальной таблицам. При этом будем выполнять запросы при различных комбинациях параметров.
Сторона СУБД
Начнем анализ с виртуальной таблицы «Обороты», так как она присутствует вне зависимости от вида регистра.
Виртуальная таблица «Обороты»
Виртуальная таблица «Обороты» есть, как у регистра накопления с видом «Обороты», так и с видом «Остатки». Рассмотрим оба варианта. Начнем с последнего.
Вид регистра «Остатки»
Посмотрим состав полей таблицы оборотов на примере регистра «ОстаткиНоменклатуры».
В нем содержатся поля каждого из измерений, а также поля «Приход», «Расход» и «Оборот» для каждого из ресурсов в регистре. В нашем случае у нас два измерения («Номенклатура» и «Склад»), а также три поля «КоличествоПриход», «КоличествоРасход» и «КоличестоОборот».
В зависимости от установленного параметра «Периодичность» в состав доступных полей вирт. таблицы будут добавляться соответствующие периоды («ПериодДень», «ПериодМесяц» и т.д.).
Теперь напишем простой запрос для получения оборотов по номенклатуре за период. В параметрах виртуальной таблицы установим поля «НачалоПериода» и «КонецПериода», а в условия добавим отбор по складу «Склад №1». При выполнении запроса платформа сформирует два SQL-запроса к базе данных. Первый запрос получает настройки регистра накопления:
Используя эти настройки, платформа формирует SQL-запрос непосредственно на получение оборотов. Вот так выглядит SQL-запрос платформы для получения оборотов:
Старался подробно разобрать весь запрос. Если будут непонятные моменты, то прошу в комментарии. Исходный текст запроса на языке 1С:Предприятия выглядит следующим образом:
В случае, если для виртуальной таблицы также устанавливается параметр «Периодичность», например, в значение «Месяц», то SQL-запрос немного видоизменится:
Если в виртуальной таблице периодичность установлена «Авто», то в SQL-запросе будут содержаться поля периода для каждой из получаемой в запросе периодичности («День», «Месяц», «Год» и т.д.). Причина, по которой платформа хранит значения периода с увеличением части даты «Год» на 2000 лет мне не известна. Если кто из читателей подскажет, буду благодарен.
Мы с Вами рассмотрели SQL-запросы платформы при работе с виртуальной таблицей «Обороты» регистра накопления с видом «Остатки». Как мы видим, виртуальная таблица «Обороты» в этом случае берет данные из таблицы движений регистра. Даже, если обороты в запросе получаются за несколько месяцев, необходимые данные будут также формироваться по таблице движений без использования каких-либо сохраненных ранее итогов. И это понятно, регистр накопления с видом «Остатки» предназначен для учета остатков, а не оборотов. Далее мы увидим, почему для решения задач учета оборотов лучше использовать соответствующий вид регистра накопления.
Вид регистра «Обороты»
Виртуальная таблица «Обороты» включает в себя поля для каждого из измерений регистра, а также по одному полю на каждый ресурс. Для оборотного регистра «ДвиженияНоменклатуры» из тестовой конфигурации состав полей таблицы следующий:
Как и в предыдущем случае, в зависимости от параметра «Периодичность» в составе доступных полей появятся соответствующие периоды.
И так, выполним несколько запросов к таблице «Обороты» и проанализируем SQL-запросы платформы. Первый запрос на языке запросов платформы:
Первым делом платформа 1С:Предприятие получит настройки регистра накопления, к которому выполняется запрос. Запрос будет идентичный рассматриваемому ранее примеру, пойдем дальше. Сформированный платформой SQL-запрос тогда будет такой:
К комментариям в приведенном тексте добавлю, что вне зависимости от значений параметров «НачалоПериода» и «КонецПериода» запрос пытается получить данные и из итоговых таблиц, и из таблицы движений регистра.
Если в запросе на языке платформы мы добавим использование параметра «Периодичность» (например, поставим значение «Месяц»), то SQL-запрос платформы изменится аналогично рассмотренному примеру для регистра накопления с видом остатки. Будут добавлены поля выбранных периодов («ПериодДень», «ПериодМесяц» и т.д.) в секции запроса «SELECT» и «GROUP BY». Для нашего примера это месяц. Поля и группировки будут добавлены для всех вложенных запросов и, конечно, содержаться в результатирующей выборке. В нашем примере, выражения в поле запроса для получения периода будет таким:
Принцип получения значений периода был описан выше для регистра с видом «Обороты».
Заключение
Если представить действия SQL-запросов схематично, то выглядеть это будет примерно так:
По схеме видно, что наиболее оптимальным образом платформа работает с виртуальной таблицей «Обороты» для регистра накопления с видом «Обороты», поскольку использует рассчитанных итоги по оборотам в разрезе месяцев. И лишь в тех случаях, когда рассчитанных итогов для периода нет, тогда использует таблицу движений. Для регистра вида «Остатки» всегда используется таблица движений вне зависимости от настроек хранения итоговых записей. Именно поэтому следует внимательно отнестись к настройке вида регистра накопления при проектировании структуры метаданных конфигурации.
Интересен тот факт, что если для регистра накопления отключить использование итогов, то тогда запрос к виртуальной таблице «Остатки» станет невозможным. Будет появляться такая ошибка:
Причем, данная ошибка будет не смотря на то, что виртуальная таблица «Обороты» для регистра накопления с видом «Остатки» использует только таблицу движений.
Что дальше
В следующих статьях будет рассмотрена работа платформы с виртуальными таблицами «Остатки» и «Остатки и обороты». Также коснемся работы агрегатов регистра накопления.
Запросы, обработки
7.1. Основные сведения о запросах
Запросы создают с некоторой целью. Например, она может звучать так: «Узнать количество и стоимость материалов, числящихся за Ивановым И.И.». После того, как цель запроса сформулирована, нужно выполнить определенные шаги, которые позволяют получить нужную информацию:
Источники данных для запросов
Написание текста запроса
Выполнение запроса и обработка результатов запроса
7.2. Создание внешней обработки КонсольЗапросов
Войдем в Конфигуратор и выполним команду Файл > Новый > Внешняя обработка, рис. 7.1.
Введем в поле имя КонсольЗапросов, для начала редактирования формы обработки нажмем на кнопку Открыть в поле Основная форма внешней обработки. Появится окно Конструктор формы обработки, оставим в нем все по умолчанию и нажмем Готово.
Теперь зададим обработчик нажатия кнопки Выполнить. Для этого откроем окно свойств кнопки и нажмем на кнопку Открыть в поле Действие. Процедура обработчика события нажатия на кнопку будет выглядеть следующим образом:
Поясним ее команды. Они, в основном, связаны с новым для вас объектом Запрос.
Записываем в свойство запроса Текст данные, которые хранятся в поле текстового документа.
Благодаря этой команде в поле РезультатВыполненияЗапроса можно увидеть его содержимое, иначе оно будет выглядеть пустым.
После того, как создание обработки завершено, сохраним ее командой главного меню программы Файл > Сохранить.
Файлы внешних обработок имеют расширение *.EPF, рис. 7.4.