Pull to refresh

Comments 73

Почему автор оригинальной статьи просит рефлексию только на этапе компиляции? Многие современные языки поддерживают рефлексию в Runtime: Java, C#, Python, Objective-C, ActionScript 3. Даже фреймворк Qt умеет создавать метаинформацию на этапе компиляции (так называемая предкомпиляция) и использовать её в Runtime.
Кратко — потомучто это с++.
Если вам _действительно_ нужна рефлексия на этапе исполнения, то, наверное, с++ это не ваш случай.

P.S. сам начав читать эту заметку чуть со стула не упал, но потом, как дочитал до «рефлексия на этапе компиляции», отпустило.
Не аргумент. Чем рефлексия лучше/хуже [традиционной] отладочной информации, которая уже сейчас может включаться/не включаться?
Если и иметь рефлексию на этапе выполнения, то я бы предпочел самостоятельно определять, что нужно включать в доступные для рефлексии данные, а что нет. В этом вся мощь C++ — все решает программист, а не компилятор.
Основной принцип C++: ты «платишь» только за то, что используешь. Долгое время это было неправдой по отношению к исключениям, но вроде сейчас на большинстве платформ их таки сделали zero-cost.

Рефлексия же, за которую ты можешь не платить всегда будет ограниченной и в этом случае она, как средство языка, не нужна: если нет возможности узнать что-то про любой объект, а только лишь про некоторые, особо помеченные, то всегда можно воспользоваться «рефлексией на этапе компиляции», чтобы сделать ограниченную рефлексию во время исполнения.
Если рассматривать рефлексию применительно не к объектам, а к типам — то все становится довольно просто. Если к типу применяется аналог typeid — то данные рефлексии о типе включаются в выходной файл. Если такой оператор ни разу не применяется — данные о нем не включаются.

И где тут ограниченность рефлексии?
И где тут ограниченность рефлексии?
Если рассматривать рефлексию применительно не к объектам, а к типам
Сами спросили — сами ответили.

Си++ — статически типизированный язык, то есть «рефлексия только для типов» = «рефлексия времени компиляции». Во время исполнения остаётся максимум интроспекция (и за неё тоже придётся дополнительно платить). Нельзя будет создать во время исполнения новый тип данных, или добавить в класс ещё одно поле, или создать объект произвольного типа данных, и т. д.
Нельзя будет создать во время исполнения новый тип данных, или добавить в класс ещё одно поле, или создать объект произвольного типа данных, и т. д.
Извините, но в каком языке программирования такое вообще возможно?
Практически в любом динамически типизированом языке.
То есть, по вашему, в статически типизированных языках рефлексия невозможна в принципе?..
Вы меня не поняли.

— Извините, но в каком языке программирования такое вообще возможно?
— Да практически в любом динамически типизированом языке это уже давно есть (например Python).
в джаве так точно можно, в c# вроде тоже, но я не уверен
ан нет, вру, в java такого нет, а вот в шарпе точно можно в класс добавить метод в рантайме
Как вы это себе вообще представляете? Вы с C++ вообще когда-либо работали или только вопросы на StackOverflow видели? Если я в программе первый раз для typeid вызвал, а до того в библиотеке его никто ни разу не вызывал, то что должен, спрашивается, компилятор делать? Посылать запрос разработчикам библиотеки?
Зачем? Компилятор что, не знает полей и методов запрошенного типа? Надо сгенерировать нужные структуры и все.
Компилятор может не знать ни полей, ни методов запрошенного типа, если библиотека так спроектирована, что ему приходят только указатели на объекты, а не сами объекты.

Более того: стандартная библиотека даже не C++, а C пример одного такого объекта содержит: FILE.

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

Так, MyClass&ltFoo&gt, например, может определять MyClass<...>::F только для таких типов Foo, которые имеют внутри себя тип Bar. Это называется «шаблонная магия». Используются такие штуки в сложных header-only библиотеках типа boost.

Именно этот факт и не дает разработчикам компилятора реализовать то, что в этой статье называется рефлексией.

Что мешает делать свою таблицу метаданных для каждого инстанса шаблона отдельно?
Компилятор не знает заранее для каких типов вы будете пытаться запрашивать информацию через рефлексию, а «вариаций» возможных инстансов шаблона может быть ОЧЕНЬ много. Можно, конечно, договориться о том, что можно получать информацию только о тех классах, которые были явно использованы в коде, но это сильно ограничит применение такой технологии.
Так, стоп. Сейчас (как и в статье) речь идёт о рефлексии времени компиляции? Если да, то компилятору известны абсолютно все шаблоны, для которых могут попытаться запросить интроспективную информацию во время исполнения программы.

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

Например, я хочу реализовать сериализацию произвольных объектов в json.
C рефлексией времени выполнения это выглядит как-то так:
void write_data<T, TStream>(T object, TStream &s)
{
    const auto &fields = typeid(T).fields;
    for(auto it = fields.begin(); it != fields.end(); ++it)
    {
        auto fieldType = it->type;
        auto value = it->getValue(object);
        auto write = typeid(TStream).getFunction("write", fieldType.asConstRef());
        write.invoke(s, value);
    }
}
Как это должно быть переписано чтобы цикл «выполнялся» во время копиляции — разворачивался в код?
При помощи магии шаблонов, очевидно же. Но мне тоже нравится рефлексия времени выполнения — с ней код как-то проще выглядит.

PS ваш код прекрасно отработает в том варианте реализации рефлексии, который я предлагаю, поскольку T и TStream в каждом конкретном инстансе функции — конкретные типы, прекрасно известные компилятору.
Типы-то известные, но вот getFunction/invoke в данном случае вызываются во время выполнения.

Как их вызывать во время компиляции? Учтите, что даже имя свойства здесь переменная времени выполнения — в шаблон ее не передашь в качестве шаблонного параметра. Конечно, можно придумать какие-то дополнительные интристики, типа тех, которые сейчас позволяют опеределить внутри шаблонов некоторые свойства типов (is_pod и т.п.). Но это никакая не рефлексия, это скорее расширение type_traits. Причем, для своей работы ей потребуются вещи типа boost::mpl::vector, т.к. список полей — это уже вектор. Выглядеть в конечном итоге это будет настолько нечитабельно, что уж лучше ручками для каждого класса сериализатор написать…
Для разворачивания в код нужны макросы / Reflection.Emit / адовы шаблоны + инлайнер / whatever, а не только одна интроспекция.

Рефлексия времени компиляции не подразумевает, что ей пользоваться можно только внутри шаблонных типов. Только то, что все обращения к интроспективным полям и методам (содержимому fields в вашем примере) связываются с некоторым кодом и данными ещё во время компиляции. Компилятор анализирует программу, генерирует тучу таких объектов и сваливает их в какой-нибудь reflection.o, а потом линкер свяжет все эти вызовы getFunction(), с конкретными функциями и байтиками.

В случае рефлексии времени компиляции все эти интроспективные метаобъекты, которые возвращает typeid, являются неизменяемыми (как и код исполняемой программы). Рефлексия времени исполнения позволяет программе самой создавать метаобъекты, изменять существующие, конструировать объекты по метаобъектам и прочее, что только в голову взбредёт.
Теоретически такое можно сделать, но это очень большое изменение для компилятора, вот навскидку проблемы которые придется решать:

— что если сгенерированный код сам по себе пытается генерировать код?
— как потом отлаживать нагенерированный код?
— как выводить сообщения об ошибках?
— как отлаживать саму генерацию кода?
— необходима стандартизация AST
— необходимо какое-то расширение constexpr, позволяющее генерировать функции (а не значения, как сейчас)
— вероятнее всего для полноценной понадобится механизм атрибутов

и т.д.

в итоге напрашивается вопрос: а стоит ли оно того? может есть какие-то другие подходы?
Для Си++ — не думаю. Он и так достаточно сложный, чтобы туда ещё недо-Template Haskell примешивать. Макросы, отладчик макросов, стандартизованное AST, полноценные функции — всё это есть в Common Lisp, но как показала практика (читай: популярные промышленные языки), оно не нужно.
+1 «любая достаточно сложная система содержит внутри себя недоинтерпретатор common lisp»
С++ надо не усложнять, а упрощать. Как вариант — переходить на D, но тут нужна поддержка от вендеров.
Ну, или можно сделать «продвинутый препроцессор» как в D: constexpr-функция возвращает строку, строка отправляется компилятору как кусок исходника.
Этот подход не взлетит в С++: в строку придется заодно передать все инклуды того куска кода, который вызвал кусок кода который генерирует код. Вот такая абракадабра. Кодогенерация не будет нормально работать до тех пор пока не сделают что-то с механизмом #include.
Прочитайте еще раз, что именно я предлагал сделать. В моем варианте компилятор как раз прекрасно знает, для каких типов запрашивается информация.
Смотрите выше мой пример сериализации. Как вы будете использовать информацию полученную через рефлексию? Какой толк от того, что вы знаете имена полей, если С++ не имеет механизма получения значения поля по его имени?
Вы вообще о чем разговариваете?

Сейчас — да, в C++ нет ни рефлексии, ни способа получения значения поля по его имени.
Если сделать как я предложил — то будет и рефлексия, и способ получения поля по его имени. Вы его, кстати, даже написали: auto value = it->getValue(object) Тут, конечно, есть еще проблема, какого же типа это value в итоге будет — вероятно, это должно быть что-то вроде boost::any, только содержащее в себе еще и информацию для дальнейшей рефлексии.
Но заголовочный файл вы же подключили? Иначе как вы пользуетесь библиотекой? А там описано всё, что нужно.
Заголовочный файл я подключил. Там написано

class File;
Ваши дальнейшие действия?

P.S. А «class Window» может быть описан, если его потомки там не упоминаются и скрыты в недрах библиотеки (далеко не такое редкое состояние), то что вы сможете сделать.
Если известно лишь имя класса (forward declaration), то вы не сможете вообще ничего с ним сделать. Ни получить доступ к полю, ни метод вызвать. Да что уж там, даже создать объект не сможете. Максимум — завести на него указатель. Что вы в этом случае собираетесь рефлексировать?
У вас тут слетела с катушек логика. Пропущены два ключевых слова. Надо так:

Без рефлексии если известно лишь имя класса (forward declaration), то вы не сможете вообще ничего с ним сделать. Без рефлексии ни получить доступ к полю, ни метод вызвать. Без рефлексии вы даже создать объект не сможете.

И тогда вопрос что вы в этом случае собираетесь рефлексировать? отпадает: в языках «со сквозной» рефлексией времени исполнения (будь то Java или там Python) вы сможете сделать все эти вещи имея изначально только название класса. Собственно многие пакеты для этих языков используют рефлексию на классы, которые не то что не были описаны в момент компиляции кода, но даже не существовали когда это происходило.

Возьмите типичный пример рефлексии, Ну, скажем, Google Guice. Он вызывается из программы, которая, конечно, про Google Guice знает. Но работает он зачастую с пользовательскими классами, которые про него ничего не знают. И при этом про эти классы пользовательская программа не знает ничего тоже! И кто и где у вас будет порождать все необходимые для этого структуры?
Стало быть, для вас рефлексия — это всего лишь средство нарушения инкапсуляции?
При чём тут нарушение инкапсуляции, я вас умоляю? Вы не сможете таким образом работать с приватными полями объектов, а если у вас класс публичный и конструктор публичный тоже, то почему вдруг его вызов программой, которая была написана до его создания стал нарушением чего-либо?
Потому что если в заголовочном файле библиотеки почему-то написано struct foo; — и ничего кроме — то это написано неспроста. Такой класс — полностью внутренний для библиотеки, и любая возможность «рефлексии» в его отношении — это совсем не то, что хотел автор библиотеки.

Если же в заголовочном файле класс описан полностью — то и на рефлексию никаких ограничений не будет.
В общепринятом подходе рефлексия позволяет нарушать инкапсуляцию, т.к. компилятор не имеет возможности догадаться, инициированы ли вызовы к рефлексии «внутренним» или «внешним» кодом. Классические примеры — сериализация и ORM, которым просто необходим доступ к внутренностям классов.
Guice поступает не совсем честно. В C++ вы можете завести указатель на функцию внутри класса, а потом присвоить ей адрес любого функтора, тем самым подставив любую реализацию. Не знаю, как на самом деле реализован Guice, но выглядит примерно так же — с помощью аннотаций в классе оставляется лазейка.

(будь то Java или там Python) вы сможете сделать все эти вещи имея изначально только название класса

Вы не можете в Java иметь только имя класса. При первом упоминании класса, VM пытается подгрузить *.class-файл. Если не найдёт его — вылетит с исключением. А в C++ мы можем завести указатель на несуществующий класс.

Python особняком тут стоит. Он использует утиную типизацию, а она без рефлексии вообще не реализуема (я так думаю).
Такое ощущение что вы даже не представляете как устроена рефлексия в Java.

В Java всё ровно наоборот по сравнению с C++.

В Java вы можете завести указатель на объект неизвестного класса, но можете его создать и с ним работать, так как Class.html.forName принимает строку, даёт вам доступ к конструкторам (через getConstructors) и позволяет создать объект (через newInstance). Вот это — и есть полноценная рефлексия времени исполнения.

В С++ же вы не можете легко описать указатель на объект какого угодно класса, но чтобы хоть что-то потом с этим классом сделать вы должны-таки поиметь его описание. То есть ни о какой полноценной рефлексии времени исполнения и речи не идёт.

Что касается аннотаций в Guice — то они строго опциональны. Они позволяют вам уменьшить количество писанины, но и только. Если вы встраиваете в программу испольщую Guice стороннюю библиотеку, то вам волей-неволей приходится обходиться без аннотаций. А как иначе: вы же не можете вот так взять и поменять стороннюю библиотеку!
s/В Java вы можете завести указатель на объект неизвестного класса/В Java вы не можете завести указатель на объект неизвестного класса/
вы должны-таки поиметь его описание

В java описанием класса, а заодно и реализацией, является class-файл. Без него Class.forName бесполезен. В java нельзя описать имя класса отдельно от класса.

В C++ можно. Но стоит различать эти две вещи — имя класса и сам класс. Описав сущность «имя класса» мы можем получить лишь имя класса. Ну серьёзно, что вы хотите от компьютера, если даже сам программист знает лишь имя и ничего более?
А если мы имеем заголовочный файл с описанием интерфейса класса, то этой информации уже будет достаточно для реализации рефлексии.
Не знаю, как в Java, но в C# можно полноценно пользоваться объектом через рефлексию не имея его определения вообще.

Вот пример:
private string ResolveEmailAddressFromOutlook()
{
    dynamic application = Marshal.GetActiveObject("Outlook.Application");
    foreach (var account in application.Session.Accounts)
    {
        var address = (string) account.SmtpAddress;
            if (address.EndsWith("@mycompany.com"))
                return address;
        return null;
}
Этот код возвращает имейл пользователя из открытого инстанса оутлука версии 2007+.
При этом этот код не ссылается даже на ком-интферфейс, т.е. при отсутствии оутлука как такового он просто кинет исключение.

Подробнее:
dynamic application = Marshal.GetActiveObject("Outlook.Application");
Эта строчка создает объект Outlook.Application объявленный в сборке Interop.Oulook, при этом сама сборка подключается к приложению динамически при первом выполнении этой строки.

Обратите внимение на ключевое слово dynamic. Оно указывает на то, что тип объекта будет определен во время выполнения и все операции с его «внутренностями» должны выполняться через рефлексию.

Так, например,
application.Session
Превращается в нечто вроде:
(application as object).GetType().GetProperty("Session").GetValue(application)
Таким образом, dynamic — это просто синтаксический сахар над рефлексией. Реальный тип такого объекта во время компиляции — object.

Естественно, что тоже самое работает и на обычных (не COM) типах, разница будет лишь в том, что для получения инстанса чего-то вроде application вам потребуется указать его полное имя и сборку, где находится его тип: Activator.CreateInstance(string typeName)
Более того, C# (думаю, что Java тоже) позволяет во время выполнения создавать абсолютно новые типы и генерировать их код. На это принципе основана работа подавляющего числа ORM библиотек.
Ну и чтоб уж совсем добить:
1. Рефлексия вопреки общепринятому мнению не такая медленная как кажется. Т.е. она, конечно, медленная, но никто не запрещает кешировать делегаты полученные с ее помощью.
2. Генерация кода позволяет не только генерировать IL (аналог ассемблера для байт кода), но и
а. Компилировать на лету любые однострочечные выражения (см. Expression.Compile)
б. Компилировать произвольный по сложности С# код, используя компилятор, идущий в поставке с .NET (см. CScript)
в. В будущем — использовать «компилятор как сервис» (cм. Roslyn)
в java нет, нельзя создать тип который не описан
orm работают либо на готовых и описанных классах entity, сканируя через рефлексию их поля\методы, либо возвращают map
просто так создать в рантайме класс нельзя, можно его во время компиляции создать и использовать в рантайме
Да вы что? А как по вашему java.lang.reflect.Proxy работает?
покажите код создающий в рантайме класс, прибивающий ему методы и выполняющий их, пожалуйста
Загляните в исходники java.lang.reflect.Proxy
ну так как насчёт вашего кода?
Я привел код, который делает то, что вы попросили. Что еще вам надо?
вы какой-то странный
я просил показать код на java, который создаёт класс в рантайме, добавляющий к нему методы и вызывающий их
а вы мне пишете чтобы я заглянул в исходники, вы нормальны вообще? если не можете написать такое — так и скажите
исходники я уже посмотрел, там нет динамического создания класса
Загляните в исходники java.lang.reflect.Proxy. Там находится класс, который
создаёт другой класс в рантайме, добавляет к нему методы и возвращает запрошенный интерфейс, через который эти методы можно вызвать

Или вы у меня просите программу, у которой вообще не будет исходников?
я прошу программу, в коде которой динамически создаётся класс, который до выполнения не был описан
я вижу в коде Proxy методы, которые создают объекты реализующие переданный интерфейс, нет интерфейса — прокси не подходит
Во-первых, вы не просили, чтобы у этого класса не было никакого интерфейса — вы лишь просили, чтобы этот класс был новым и нигде не описывался. А это действительно так: для реализации своих задач Proxy действительно создает в рантайме новые классы.

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

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

так её и не я поставил, а пост выше, вот этот:
>Более того, C# (думаю, что Java тоже) позволяет во время выполнения создавать абсолютно новые типы и генерировать их код.
нет никаких принципиальных сложностей это сделать.

ну и где код то?
Для C# есть пример в MSN цитирую:
example shows how to create a dynamic type with a field, constructor, property, and method.

Для java есть несколько библиотек, которые умеют создавать классы в рантайме, цитирую:
it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it.
Еще варианты (Stack Overflow)

(в java именно «создавать» типы с помощью стандартного API нельзя, зато можно загружать любой байт-код с любыми типами, а вышеуказанные библиотеки позволяют его генерировать в рантайме, к сожалению я далек от джавы поэтому не могу сказать насколько активно ими пользуются)
Ну либо можно прибегнуть к извратам вроде «C++/CLI»
Имея рефлексию на этапе компиляции, не так уж сложно сделать рефлексию на этапе выполнения. Причем не для всех объектов языка, а только там где это действительно нужно. А это как раз и есть C++ way.
Полноценная рефлексия в С++ теоретически невозможна из-за шаблонов и шаблонной магии:
1. содержимое шаблонных классов зависит от того, для какого типа такой класс инстанциируется
2. не инстанциированные шаблоны не попадают в бинарные файлы ни в каком виде
3. для определения того, какую специализацию шаблона интанциировать нужны полноценные исходники, т.к. существует SFINAE
Рефлексия этапа выполнения не бесплатна. Этапа компиляции — даже наоборот, может быть использована для оптимизации итоговой программы. При этом из второй можно получить первую с помощью библиотечного решения, а наоборот — никак.
Спасибо за перевод.
Только я немного не понял из статьи — почему нам, всё-таки, нужен Reflection?: о)
Рефлексия очень помогает, если надо расширить язык — например, реализовать собственный RPC-механизм или сделать скриптовый язык, который прозрачно и безболезненно интегрируется в код — все объекты и типы данных будут видны из скрипта практически автоматически. Но для этого требуется мощная рефлексия времени выполнения, а она за один присест не появится, она требует некоторой эволюции, например, как в Qt.
Рефлексия очень помогает, если надо расширить язык — например, реализовать собственный RPC-механизм

Да, было бы неплохо иметь, что-то на языковом уровне. А пока приходится самостоятельно все разбирать с помощью макросов и шаблонов. Не так давно как раз я и публиковал пост о реализации RPC, прибегая к макросам и завернутым в них шаблонам.
Одно из применений рефлексии это ORM. Я когда-то искал какой-нибудь удобный ORM фреймворк для C++/Qt, но в большинство из них представляло из себя набор макросов, которыми крайне неудобно пользоваться.
Ещё во фреймворке Qt есть модуль QtQuick для декларативного описания интерфейсов на JavaScript-подобном языке QML. Так вот благодаря метаклассам можно из JavaScript обращаться к обычным свойствам и методам C++ классов. Это ли не чудо? =)
Один из самых часто возникающих случаев, где необходима рефлексия, — это всяческая сериализация/десериализация. Представьте, например, что в вашей программе есть структура типа такой:

struct CatParameters
{
	string	name;
	Gender	gender;
	int	age;
	string	color;
	double	tail_length;
	double	weight;
	double	appetite;
	// ...
};


Также у вас есть некоторое key-value хранилище с примерно следующим интерфейсом:

template<typename T>
T get_value(const char* key);


Теперь представьте, что вам нужно реализовать функцию загрузки параметров кота из хранилища:

CatParameters load_cat_parameters()
{
	CatParameters p;
	// ???
	return p;
}


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

p.name = get_value<string>("name");
p.gender = get_value<Gender>("gender");
p.age = get_value<int>("age");
//...


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

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

static_for(field : CatParameters)
	p.*field = get_value<decltype(field)>(as_string(field));


Эдакий типизированный макропроцессор потребует немалых изменений языка, и не очень вероятно, что что-либо подобное может появиться в C++1y. А жаль, поскольку очень не хватает подобных средств.
Для меня эффективная рефлексия этапа компиляция — это главная причина предпочтения D (vs C++ / Rust / Go). Однажды привыкнув пользоваться подобным инструментом, отказаться практически невозможно.
А мы придумали собственный костыль для рефлексии: с помощью пары магических BOOST макросов при описании структуры получаем type-list с плюшками.

Типа так:
struct MyStruct {
  REFLECT(
    (int) _x,
    (boost::optional<whatever>) _something,
  )
};


Благодаря этой штуке мы получили JSON конвертор (для логов объектов и пр.), ORM для доступа к БД (SQL генерируется автоматически и переменные привязываются к запросу), валидация контейнеров, ну и список типов.

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

Безусловно, хотелось бы иметь это на уровне языка.
Sign up to leave a comment.