Почему в С ++ нет отражения?
Это несколько странный вопрос. Моя цель состоит в том, чтобы понять решение по проектированию языка и определить возможности отражения в C++.
Почему комитет по языку C++ не пошел на реализацию отражения в языке? Является ли отражение слишком сложным в языке, который не работает на виртуальной машине (например, Java)?
Если кто-то реализует рефлексию для C++, какие будут проблемы?
Я думаю, что использование отражений хорошо известно: редакторы могут быть написаны легче, программный код будет меньше, макеты могут генерироваться для модульных тестов и так далее. Но было бы здорово, если бы вы тоже могли прокомментировать использование рефлексии.
16 ответов
Есть несколько проблем с отражением в C++.
Нужно добавить много работы, и комитет C++ довольно консервативен и не тратит время на радикально новые функции, если они не уверены, что он окупится. (Было предложено добавить систему модулей, аналогичную сборкам.NET, и, хотя я думаю, что существует общее мнение, что было бы неплохо иметь это, это не является их главным приоритетом в настоящее время, и было отложено до тех пор, пока C++0x. Мотивация этой функции - избавиться от
#include
система, но это также позволило бы по крайней мере некоторые метаданные).Вы не платите за то, что не используете. Это одна из основных принципов проектирования, лежащих в основе C++. Зачем в моем коде метаданные, если они мне могут никогда не понадобиться? Более того, добавление метаданных может помешать компилятору оптимизировать. Зачем мне платить эту стоимость в моем коде, если мне никогда не понадобятся эти метаданные?
Что приводит нас к другому важному моменту: C++ дает очень мало гарантий относительно скомпилированного кода. Компилятору разрешено делать практически все, что ему нравится, при условии, что полученная функциональность соответствует ожидаемой. Например, ваши классы не обязательно должны быть там. Компилятор может оптимизировать их, встроить все, что они делают, и он часто делает именно это, потому что даже простой код шаблона имеет тенденцию создавать довольно много экземпляров шаблона. Стандартная библиотека C++ опирается на эту агрессивную оптимизацию. Функторы являются эффективными, только если можно оптимизировать накладные расходы на создание и уничтожение объекта.
operator[]
по вектору сравним только с производительностью индексирования массивов по производительности, поскольку весь оператор может быть встроен и, таким образом, полностью удален из скомпилированного кода C# и Java дают много гарантий относительно вывода компилятора. Если я определю класс в C#, то этот класс будет существовать в результирующей сборке. Даже если я никогда не использую это. Даже если все вызовы его функций-членов могут быть встроены. Класс должен быть там, чтобы рефлексия могла его найти. Частично это облегчается компиляцией C# в байт-код, что означает, что JIT-компилятор может удалять определения классов и встроенные функции, если ему это нравится, даже если исходный C# -компилятор не может. В C++ у вас есть только один компилятор, и он должен выводить эффективный код. Если бы вам было позволено проверять метаданные исполняемого файла C++, вы бы ожидали увидеть каждый класс, который он определил, а это значит, что компилятор должен будет сохранить все определенные классы, даже если они не нужны.И тогда есть шаблоны. Шаблоны в C++ не похожи на шаблоны в других языках. Каждый экземпляр шаблона создает новый тип.
std::vector<int>
это совершенно отдельный класс отstd::vector<float>
, Это добавляет много разных типов во всей программе. Что должно увидеть наше отражение? Шаблонstd::vector
? Но как это может быть, поскольку это конструкция с исходным кодом, которая не имеет смысла во время выполнения? Надо бы увидеть отдельные классыstd::vector<int>
а такжеstd::vector<float>
, А такжеstd::vector<int>::iterator
а такжеstd::vector<float>::iterator
то же самое дляconst_iterator
и так далее. И как только вы вступаете в метапрограммирование шаблонов, вы быстро заканчиваете создание сотен шаблонов, каждый из которых снова вставляется и удаляется компилятором. Они не имеют никакого значения, кроме как как часть метапрограммы времени компиляции. Должны ли все эти сотни классов быть видимыми для размышлений? Им придется это сделать, потому что иначе наше отражение будет бесполезным, если оно даже не гарантирует, что классы, которые я определил, действительно будут там. И побочной проблемой является то, что класс шаблона не существует, пока он не будет создан. Представьте себе программу, которая используетstd::vector<int>
, Должна ли наша система отражения видетьstd::vector<int>::iterator
? С одной стороны, вы, конечно, ожидаете этого. Это важный класс, и он определяется с точки зренияstd::vector<int>
, который существует в метаданных. С другой стороны, если программа на самом деле никогда не использует этот шаблон класса итератора, его тип никогда не будет создан, и поэтому компилятор не будет генерировать класс в первую очередь. И уже слишком поздно создавать его во время выполнения, так как он требует доступа к исходному коду.- И, наконец, рефлексия не так важна в C++, как в C#. Причина снова в шаблонном метапрограммировании. Он не может решить все, но во многих случаях, когда вы иначе прибегаете к рефлексии, можно написать метапрограмму, которая делает то же самое во время компиляции.
boost::type_traits
простой пример. Вы хотите знать о типеT
? Проверьте егоtype_traits
, В C# вам придется ловить рыбу после его типа, используя отражение. Отражение все еще было бы полезно для некоторых вещей (основное использование, которое я вижу, которое метапрограммирование не может легко заменить, для автоматически сгенерированного кода сериализации), но это будет нести некоторые значительные затраты для C++, и это просто не нужно так часто, как есть на других языках.
Изменить: В ответ на комментарии:
cdleary: Да, символы отладки делают нечто подобное в том смысле, что они хранят метаданные о типах, используемых в исполняемом файле. Но они также страдают от проблем, которые я описал. Если вы когда-нибудь пытались отладить сборку релиза, вы поймете, что я имею в виду. Существуют большие логические пробелы, в которых вы создали класс в исходном коде, который был выделен в конечном коде. Если бы вы использовали рефлексию для чего-то полезного, вам нужно, чтобы оно было более надежным и последовательным. Как таковые, типы будут исчезать и исчезать почти каждый раз, когда вы компилируете. Вы меняете крошечную деталь, и компилятор решает изменить, какие типы являются встроенными, а какие нет, в ответ. Как извлечь из этого что-то полезное, если вы даже не гарантированы, что наиболее важные типы будут представлены в ваших метаданных? Тип, который вы искали, возможно, был там в последней сборке, но теперь его нет. А завтра кто-нибудь проверит небольшое невинное изменение на маленькую невинную функцию, которая делает тип достаточно большим, чтобы он не стал полностью встроенным, поэтому он вернется снова. Это все еще полезно для отладочных символов, но не намного больше этого. Я бы не хотел пытаться генерировать код сериализации для класса в соответствии с этими условиями.
Эван Теран: Конечно, эти проблемы могут быть решены. Но это возвращается к моей точке № 1. Это заняло бы много работы, и у комитета C++ есть много вещей, которые они считают более важными. Является ли выгода от того, что какое-то ограниченное отражение (и оно будет ограниченным) в C++ действительно достаточно велико, чтобы оправдать сосредоточение на этом за счет других функций? Действительно ли есть огромное преимущество в добавлении функций основного языка, которые уже (в основном) могут быть сделаны через библиотеки и препроцессоры, такие как QT? Возможно, но необходимость гораздо менее актуальна, чем если бы таких библиотек не было. Тем не менее, для ваших конкретных предложений, я считаю, что запрет на использование шаблонов сделает его абсолютно бесполезным. Например, вы не сможете использовать отражение в стандартной библиотеке. Какое отражение не позволит вам увидеть std::vector
? Шаблоны являются огромной частью C++. Функция, которая не работает с шаблонами, в основном бесполезна.
Но вы правы, некоторая форма отражения может быть реализована. Но это было бы серьезным изменением в языке. Как и сейчас, типы являются исключительно конструкцией времени компиляции. Они существуют для пользы компилятора и ничего больше. После того, как код скомпилирован, классов нет. Если вы растягиваете себя, вы можете утверждать, что функции все еще существуют, но на самом деле все, что есть, - это набор инструкций для ассемблера прыжка и множество push/pop стеков. При добавлении таких метаданных не так много всего.
Но, как я уже сказал, есть предложение об изменениях в модели компиляции, добавлении автономных модулей, хранении метаданных для выбранных типов, что позволяет другим модулям ссылаться на них без необходимости связываться с ними. #include
s. Это хорошее начало, и, честно говоря, я удивлен, что стандартный комитет не просто выбросил предложение о том, что оно слишком велико. Так что, возможно, через 5-10 лет?:)
Отражение требует, чтобы некоторые метаданные о типах были сохранены где-то, что можно запросить. Поскольку C++ компилируется в собственный машинный код и претерпевает значительные изменения в связи с оптимизацией, представление высокого уровня приложения в значительной степени теряется в процессе компиляции, следовательно, запросить их во время выполнения будет невозможно. Java и.NET используют представление очень высокого уровня в двоичном коде для виртуальных машин, что делает возможным этот уровень отражения. В некоторых реализациях C++, однако, есть нечто, называемое Информация о типе времени выполнения (RTTI), которое можно рассматривать как упрощенную версию отражения.
Все языки не должны пытаться включить все функции любого другого языка.
C++, по сути, очень, очень сложный макроассемблер. Это НЕ (в традиционном смысле) язык высокого уровня, такой как C#, Java, Objective-C, Smalltalk и т. Д.
Хорошо иметь разные инструменты для разных работ. Если у нас есть только молотки, все будет выглядеть как гвозди и т. Д. Наличие языков сценариев полезно для некоторых заданий, а отражающие ОО-языки (Java, Obj-C, C#) полезны для другого класса заданий и супер - эффективные базовые языки, близкие к машине, полезны для еще одного класса заданий (C++, C, Assembler).
C++ делает потрясающую работу по расширению технологии Ассемблера до невероятных уровней управления сложностью и абстракций, чтобы сделать программирование более масштабным, более сложными задачами, значительно более доступными для людей. Но это не обязательно тот язык, который лучше всего подходит для тех, кто подходит к своей проблеме со строгой точки зрения высокого уровня (Lisp, Smalltalk, Java, C#). Если вам нужен язык с этими функциями, чтобы наилучшим образом реализовать решение ваших проблем, то поблагодарите тех, кто создал такие языки для всех нас!
Но C++ предназначен для тех, кому по каким-либо причинам необходима сильная корреляция между их кодом и работой базовой машины. Будь то эффективность, программирование драйверов устройств, взаимодействие с низкоуровневыми службами ОС или что-то еще, C++ лучше подходит для этих задач.
C#, Java, Objective-C все требуют гораздо большей, более богатой системы времени выполнения для поддержки их выполнения. Это время выполнения должно быть доставлено в соответствующую систему - предварительно установлено для поддержки работы вашего программного обеспечения. И этот уровень необходимо поддерживать для различных целевых систем, настроенных НЕКОТОРЫМ ДРУГИМ ЯЗЫКОМ, чтобы он работал на этой платформе. И этот средний уровень - этот адаптивный уровень между операционной системой хоста и вашим кодом - среда выполнения, почти всегда написана на языке, таком как C или C++, где эффективность равна #1, где предсказуемое понимание точного взаимодействия между программным и аппаратным обеспечением может быть хорошим понял и манипулировал с максимальной прибылью.
Я люблю Smalltalk, Objective-C, и имею богатую систему времени выполнения с отражением, метаданными, сборкой мусора и т. Д. Для использования этих возможностей можно написать удивительный код! Но это просто более высокий уровень в стеке, уровень, который должен опираться на более низкие уровни, которые сами должны в конечном итоге находиться на ОС и оборудовании. И нам всегда будет нужен язык, который лучше всего подходит для создания этого уровня: C++/C/Assembler.
Приложение: C++11/14 продолжает расширять возможности C++ для поддержки абстракций и систем более высокого уровня. Потоки, синхронизация, точные модели памяти, более точные определения абстрактных машин позволяют разработчикам C++ достичь многих абстракций высокого уровня, которые некоторые из этих высокоуровневых языков использовали для монопольного домена, продолжая при этом обеспечивать близкий к производительность металла и превосходная предсказуемость (т.е. минимальные подсистемы времени выполнения). Возможно, средства отражения будут выборочно включены в будущей редакции C++ для тех, кто этого хочет - или, возможно, библиотека будет предоставлять такие сервисы времени выполнения (может быть, есть один сейчас или его начало в ускорении?).
Если вы действительно хотите понять конструктивные решения, связанные с C++, найдите копию The Annotated C++ Reference Manual от Эллиса и Страуструпа. Он НЕ соответствует последнему стандарту, но он проходит через оригинальный стандарт и объясняет, как все работает и как часто, как они так поступают.
Отражение для языков, которые его имеют, зависит от того, сколько исходного кода компилятор желает оставить в вашем объектном коде, чтобы включить отражение, и сколько механизмов анализа доступно для интерпретации этой отраженной информации. Если компилятор не хранит весь исходный код, рефлексия будет ограничена в его способности анализировать доступные факты об исходном коде.
Компилятор C++ ничего не держит (ну, игнорируя RTTI), поэтому вы не получите отражения в языке. (Компиляторы Java и C# хранят только классы, имена методов и возвращаемые типы, поэтому вы получаете немного данных об отражениях, но не можете проверить выражения или структуру программы, а это означает, что даже в этих языках с "отражением" информация, которую вы можете получить, довольно скудна, и, следовательно, вы действительно не можете много анализировать).
Но вы можете выйти за пределы языка и получить все возможности отражения. Ответ на другое обсуждение переполнения стека при отражении в C обсуждает это.
Отражение может быть и было реализовано в C++ раньше.
Это не нативная функция C++, потому что она требует больших затрат (память и скорость), которые не должны устанавливаться языком по умолчанию - язык ориентирован на "максимальную производительность по умолчанию".
Поскольку вы не должны платить за то, что вам не нужно, и, как вы сами говорите, в редакторах нужно больше, чем в других приложениях, то это должно быть реализовано только там, где вам нужно, а не "принудительно" для всего кода (вам не нужно размышлять над всеми данными, с которыми вы будете работать в редакторе или другом подобном приложении).
Последние 10 лет делаются попытки добавить отражение в C++. Последнее предложение относится к c ++23С ++23 и может войти, а может и не попасть.
В отличие от отражения в большинстве языков, план отражения в C++ - это отражение во время компиляции. Таким образом, во время компиляции вы можете отражать элементы структуры, параметры и свойства функций и методов, значения и имена перечисления и т. Д.
Затем вы можете выполнить ограниченную реификацию, вводя информацию о том, над чем вы размышляли, для генерации других типов и кода.
Хотя это немного странно, это означает, что программы, не использующие отражение, не платят за это время выполнения. Это также невероятно мощно.
Самый простой пример - вы можете использовать его для реализации отражения во время выполнения.
struct Member {
std::string_view name;
std::any_ref value;
};
struct Reflectable {
virtual std::span<Member> GetMembers() const = 0;
virtual std::span<Member> GetMembers() = 0;
};
template<class D>
struct ImplReflectable:Reflectable {
std::span<Member> GetMembers() const final;
std::span<Member> GetMembers() final;
};
template<class D>
std::span<Member> ImplReflectable<D>::GetMembers() const {
// compile time reflection code on D here
}
template<class D>
std::span<Member> ImplReflectable<D>::GetMembers() {
// compile time reflection code on D here
}
вы пишете вышеупомянутое один раз, и внезапно вы для любого типа, который хотите отражать, вы можете просто сделать это:
struct Point : ImplReflectable<Point> {
int x, y;
};
и система отражения прикреплена к
Point
.
Библиотека, реализующая это отражение во время выполнения, может быть сколь угодно сложной и мощной. Каждый тип должен проделать небольшую работу (как указано выше), чтобы зарегистрироваться, но сделать это для библиотеки пользовательского интерфейса (например) не является серьезной проблемой. Типы, которые не выбирают, продолжают предположение C++ о том, что «не платите за это, если вы его не используете».
Но это только начало. Одно предложение, метаклассы, позволяет:
interface Reflectable {
std::span<Member> GetMembers() const;
std::span<Member> GetMembers();
};
у вас могут быть метаклассы или функции, которые принимают типы и возвращают их. Это позволяет вам определять метаклассы класса, такие как «интерфейс», написанные на языке. Сейчас же,
interface
это что-то вроде игрушки, но вы могли бы написать
QObject
или
Reflectable
или
PolymorphicValueType
или
NetworkProtocol
метаклассы, которые изменяют то, что означает определение вашего класса.
Это может или не может попасть в . Он продолжает улучшаться, но его также продолжают отодвигать. Существует несколько реализаций отражения времени компиляции для большинства основных компиляторов C++, которые вы можете попробовать. Синтаксис постоянно меняется, так как существуют библиотеки отражения на основе символьных операторов,
reflexpr
библиотеки отражения операторов на основе, в некоторых отраженные данные являются типами, в других -
constexpr
объекты и
consteval
функции.
Причина, по которой C++ не имеет отражения, состоит в том, что для этого потребуется, чтобы компиляторы добавляли символьную информацию в объектные файлы, например, какие члены имеют тип класса, информацию о членах, о функциях и обо всем. По сути, это сделает бесполезными включаемые файлы, так как информация, отправленная объявлениями, будет затем считываться из этих объектных файлов (затем модулей). В C++ определение типа может встречаться в программе несколько раз, включая соответствующие заголовки (при условии, что все эти определения одинаковы), поэтому необходимо будет решить, куда поместить информацию об этом типе, так же, как назвать ее осложнение здесь. Агрессивная оптимизация, выполняемая компилятором C++, который может оптимизировать десятки экземпляров шаблонов классов, является еще одной сильной стороной. Это возможно, но так как C++ совместим с C, это станет неудобной комбинацией.
Существует множество случаев использования отражения в C++, которые не могут быть адекватно решены с помощью конструкций времени компиляции, таких как шаблонное метапрограммирование.
N3340 предлагает богатые указатели как способ отражения в C++. Среди прочего, он решает проблему не оплаты функции, если вы ее не используете.
По словам Алистера Кокберна, подтип не может быть гарантирован в отражающей среде.
Отражение больше относится к системам скрытой типизации. В C++ вы знаете, какой у вас тип, и знаете, что с ним можно сделать.
Если бы C++ мог иметь:
- данные членов класса для имен переменных, типов переменных и
const
модификатор - итератор аргументов функции (только позиция вместо имени)
- данные членов класса для имен функций, типа возвращаемого значения и
const
модификатор - список родительских классов (в том же порядке, как определено)
- данные для членов шаблона и родительских классов; расширенный шаблон (то есть фактический тип будет доступен для API отражения, а не "информация шаблона о том, как туда добраться")
Этого было бы достаточно для создания очень простых в использовании библиотек суть обработки типов данных, которая так распространена в современных веб-приложениях и приложениях баз данных (все формы, механизмы обмена сообщениями, парсеры xml/json, сериализация данных и т. Д.).
Например, основная информация, поддерживаемая Q_PROPERTY
макрос (часть Qt Framework), http://qt.nokia.com/doc/4.5/properties.html расширенный для охвата методов класса и e) - будет чрезвычайно полезен для C++ и для сообщества разработчиков программного обеспечения в целом.
Конечно, рефлексия, на которую я ссылаюсь, не будет охватывать семантического значения или более сложных вопросов (таких как номера строк исходного кода комментариев, анализ потока данных и т. Д.), Но я также не думаю, что они должны быть частью языкового стандарта.
Это в основном потому, что это "необязательный дополнительный". Многие люди предпочитают C++ языкам, таким как Java и C#, чтобы они могли лучше контролировать вывод компилятора, например, небольшую и / или более быструю программу.
Если вы решите добавить отражение, существуют различные решения.
Отражение может быть необязательным, как директива препроцессора. Что-то вроде
#pragma enable reflection
Таким образом, мы можем получить лучшее из обоих миров, без этих прагматических библиотек, которые будут созданы без размышлений (без каких-либо накладных расходов, как обсуждалось), тогда индивидуальный разработчик будет решать, хотят ли они скорости или простоты использования.
Некоторые хорошие ссылки на отражение в C++, которые я только что нашел:
Отражение в C++, я считаю, крайне важно, если C++ будет использоваться в качестве языка для доступа к базе данных, обработки веб-сеансов /http и разработки GUI. Отсутствие размышлений не позволяет ORM (таким как Hibernate или LINQ), синтаксическим анализаторам XML и JSON, которые создают классы, сериализацию данных и многие другие значения (когда для создания экземпляра класса необходимо использовать изначально не типированные данные).
Переключатель времени компиляции, доступный разработчику программного обеспечения в процессе сборки, может быть использован для устранения этой проблемы "вы платите за то, что используете".
Мне разработчик прошивки не нуждается в отражении для чтения данных с последовательного порта - тогда нормально не использовать коммутатор. Но как разработчик базы данных, который хочет продолжать использовать C++, я постоянно сталкиваюсь с ужасным, сложным в обслуживании кодом, который отображает данные между членами данных и конструкциями базы данных.
Ни Boost-сериализация, ни другой механизм на самом деле не решают отражения - это должно быть сделано компилятором - и как только это будет сделано, C++ снова будет изучен в школах и использован в программном обеспечении, которое имеет дело с обработкой данных.
Для меня это проблема № 1 (а примитивы с наивным потоком - проблема № 2).
C++ - это язык, который не требует отражения, потому что C++ - это язык, который вы могли бы использовать для написания языка, в котором есть отражение.