Pull to refresh

Comments 48

Забыли самую старую и продуманую рефлексию - в Object Pascal (Delphi RTTI). Она появилась еще даже до появления Java, не говоря уже о Питоне. Очень многие вещи пошли оттуда.

Спасибо, очень интересная статья, есть о чём подумать.

Такой вопрос. Нам в рабочем проекте приходится в автоматическом режиме перегонять код C# в C++, рефлексию приходится генерировать с нуля, но это довольно плохо работает - например, нет способа проинстанциировать шаблонный тип в компайл-тайме на основе данных рефлексии. Допустим, следующий код не имеет шансов быть переведённым на C++ (хотя бы потому, что в качестве типа переменной list не понятно, что использоать, - не void* же):

using System;
using System.Collections.Generic;
using System.Reflection;

class Program
{
  static void Main(string[] args)
  {
    var boolListType = typeof(List<bool>);

    Console.Write("Enter System type name: ");
    string argument = Console.ReadLine();
    var argumentType = Type.GetType("System." + argument);

    var genericList = boolListType.GetGenericTypeDefinition();
    var requiredListType = genericList.MakeGenericType(argumentType);
    var list = Activator.CreateInstance(requiredListType);
    Console.WriteLine("Created instance of " + requiredListType.FullName);

    if (list is List<string> ls)
      Console.WriteLine("list is List<string>");
    else if (list is List<int> li)
      Console.WriteLine("list is List<int>");
    else if (list is List<Type> lt)
      Console.WriteLine("list is List<Type>");
    else
      Console.WriteLine("list is List<Something>");
  }
}

Я правильно понимаю, что через описанную в статье красоту нам в ближайшие дцать лет всё равно ничего такого не светит? Или будет какая-то подсистема, позволяющая не только анализировать типы в компайл-тайме, но и буквально составлять новые типы в рантайме?

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

Все переменные/объекты занимают какое-то количество байт на стеке, и это количество нужно знать заранее, иначе, например, непонятно как вычислить размер stack frame у функции (ассемблеру нужно это знать).

Вот такого в C++ не будет скорее всего никогда:

auto list = create_list();
// ^^^ тип list вычислится в РАНТАЙМЕ,
// будет std::vector<int>/std::vector<std::string>/другой

Аналог того, что происходит в C#/Java, на C++ выглядит скорее как использование std::any, который аллоцирует нужное количество байт для объекта в куче (в отличие от стека, там необязательно знать объем выделяемой памяти в compile-time). Можно иметь один из типов в рантайме:

std::any create_list(int n) {
    if (n == 0) {
        return std::vector<int>();
    } else if (n == 1) {
        return std::vector<std::string>();
    } else {
        return std::vector<char>();
    }
}

int main() {
    int n;
    std::cin >> n;

    auto list = create_list(n);

    if (list.type() == typeid(std::vector<int>)) {
        std::cout << "got vector of int" << std::endl;
    } else if (list.type() == typeid(std::vector<std::string>)) {
        std::cout << "got vector of string" << std::endl;
    } else if (list.type() == typeid(std::vector<char>)) {
        std::cout << "got vector of char" << std::endl;
    }
}

Спасибо. Вот я тоже подумал, что подобное вряд ли возможно, но мечтать ведь не вредно =)

Почему вряд ли возможно? Это же прямая рабочая аналогия, тип динамически задаётся в рантайме (то что под капотом там не рефлексия по сути не имеет значения, работает ведь одинаково).

Например, в Qt есть тип QVariant, который по сути удобная обёртка для таких вот вещей. И это отлично работает.

то что под капотом там не рефлексия по сути не имеет значения, работает ведь одинаково

Не одинаково. QVariant может "переварить" только те типы, которые были известны на момент его компиляции (а C++ вообще - на момент компиляции хоть какого-либо модуля). Мой же пример на C# "составляет" нужный тип уже в рантайме, подставляя аргументы в обобщённый тип. Например, если пользователь введёт слово DateTime, то будет создан экземпляр List<DateTime>, и с его членами можно будет работать (через ту же рефлексию), несмотря на то, что в компилируемом коде такая параметризация дженерика List нигде не упоминалась. В C++ же нельзя в рантайме создать экземпляр vector<T>, где T получен по имени в момент выполнения, а в момент компиляции в качестве аргумента vector не использовался.

Ситуация с вводом из консоли может показаться надуманной, но ведь имя типа может быть прочитано, к примеру, из XMLки, причём число комбинаций очень быстро выходит за тот уровень, который можно предусмотреть в коде: если vector<QDateTime> ещё можно как-то предвидеть, то какой-нибудь unordered_map<MyEnum, vector<list<map<string, set<int>>>>> - нет. Особенно, если код десериализации ничего не знает ни про какой MyEnum.

Значит я неверно понял задачу. Конечно, шаблонный тип на лету не составишь, но в таком случае не спасёт и рефлексия. Т.е. это вовсе не проблема отсутствия рефлексии, для C++ единственный вариант это заранее инстанцировать все возможные варианты шаблонов.

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

Вот как раз с десериализацией никаких проблем быть не должно: никто никогда не занимается десериализацией неизвестных типов, обычно все возможные типы известны заранее.


Если в какой-то момент нужно, к примеру, сохранить в поле кусок разметки — никто не создаёт новые неизвестные типы динамически, в том же C# используется обычный XmlElement или XElement.

Я не спорю с тем, что существуют возможности добиться подобного эффекта на C++ другими методами. Но во всех случаях это требует, чтобы на момент компиляции кода десериализации (чтения конфига и т. п.) были известны все типы, с которыми он работает, либо же предоставлялись некие фабрики для работы с такими типами. В C# такой зависимости нет (поскольку рефлексия может успешно использоваться в качестве таких фабрик), что в некоторых случаях более удобно - например, можно использовать старую библиотеку чтения конфигов с новыми типами, и даже без пересборки, не заботясь о том, как оно там работает под капотом.

В нашем проекте приходится в автоматическом режиме переводить с C# на C++ десятки миллионов строк кода. Поэтому если программисты C# где-то задействовали рефлексию времени выполнения (даже если это не было оптимальным решением) - для нас было бы куда проще, если бы транслированный код C++ также мог работать через рефлексию времени выполнения, поскольку иначе нам приходится ограничивать дотнетчиков в доступных средствах, а также тратить кучу времени на рефакторинг того, что в дотнете и так нормально работает.

поскольку рефлексия может успешно использоваться в качестве таких фабрик
Повторюсь: дело не в рефлексии, а в том, что шаблоны C++ и шаблоны C# это совершенно разные вещи.

Шаблоны в C++ — это заготовки кода, который генерируется только при инстанциации этого шаблона. Этот код учитывает размеры и расположение в памяти, а так же прочие особенности этих типов вроде конструкторов, деструкторов, операторов и т.п.

Шаблоны в C# — это обобщение типов. Это не требует заранее генерировать код на каждый возможный вариант, т.к. в generic-виде они все обрабатываются общим механизмом в рантайме.

Если бы в C# были шаблоны как в C++, никакая рефлексия бы не помогла. И, в то же время, ничего не мешает в C++ поверх языка реализовать такой же механизм обобщения типов, как в C#.

Спасибо, что пояснили. Да, с этим согласен.

UFO just landed and posted this here
Ну здесь то речь ровно об одной переменной list.
Все переменные/объекты
Тут слово «все» я понял как «любая переменная» из используемых, т.е. sizeof должен быть известен, чтобы на стеке её положить, а не то, что «все переменные функции», в этом контексте все переменные функции сразу одной пачкой и не требуются.
UFO just landed and posted this here
Вы и написали то же самое, что и Izaron — нужно заранее знать sizeof(std::vector«T»), чтобы таким образом его обработать. А исходная задача была создавать этот std::vector«T» неким вызовом активатора, причём тип T приходит снаружи в виде значения некоторого type handle (в .net он имеет тип System.Type).
UFO just landed and posted this here
Ага, понял. В строке
  container = stackAlloc(sizeof(vector<string>));
значение sizeof можно достать из метаданных типа.

Но даже это не поможет инстанцировать шаблон в рантайме.

UFO just landed and posted this here

Не можете. По крайней мере, пока не затянете в рантайм эквивалентное представление исходного кода и компилятор.

UFO just landed and posted this here

В плюсах даже шаблоны не тайпчекаются до инстанцирования, а вы хотите завтипы тайпчекать! Это явно более сложное изменение чем "один лишний индирекшн в рантайме".


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


Вы сейчас стереотипного математика из анекдота напоминаете — "а, решение существует, задачу можно дальше не решать"

UFO just landed and posted this here

Необходимость рефлексии очень преувеличена. Рантайм рефлексия - безусловное зло, не привносит никаких дополнительных возможностей, зато усложняет понимание кода многократно, сильно снижает производительность и увеличивает сложность реализации компиляторов.

Статическая рефлексия в том виде, что её предлагают тоже избыточна, пример с узнаваниями названий из enum, такая библиотека уже есть и написана она на обычном С++ без всякой рефлексии, называется magic enum.
Насчёт сериализации - таки тоже уже есть, вот пример библиотеки для бинарной сериализации, где описанием формата является тип из С++ единственное что нужно написать программисту чтобы его тип стал поддерживаться - using enable_magic = std::true_type;
Плюс всяческие алгоритмы работы с таким типом как с туплом и общие алгоритмы типа tuple cat / construct from tuple и прочие
https://github.com/kelbon/UndefinedBehavior_GoldEdition/blob/master/include/kelbon_serialization.ixx

самый актуальный пример с json. существующие библиотеки предлагают либо писать бойлерплейт самому, либо дают автогенерацию через макрос (nlohmann/json).

Да, язык хорошо живет без рефлексии много лет, но новый функционал это новые возможности.

Можно подсмотреть, что крутого есть в других языках. Фреймворк Spring в Java в этом плане очень мощно развит.

Так выглядит обработчик http-запросов:

в Java
@RestController
class EmployeeController {

  private final EmployeeRepository repository;

  EmployeeController(EmployeeRepository repository) {
    this.repository = repository;
  }

  @GetMapping("/employees")
  List<Employee> all() {
    return repository.findAll();
  }

  @PostMapping("/employees")
  Employee newEmployee(@RequestBody Employee newEmployee) {
    return repository.save(newEmployee);
  }

  @GetMapping("/employees/{id}")
  Employee one(@PathVariable Long id) {
    return repository.findById(id)
      .orElseThrow(() -> new EmployeeNotFoundException(id));
  }

  @DeleteMapping("/employees/{id}")
  void deleteEmployee(@PathVariable Long id) {
    repository.deleteById(id);
  }
}

Примерно так можно бы сделать в C++, когда доработают поддержку пользовательских атрибутов:

в C++
class [[rest_controller]] employee_controller {
public:
    void set_component(std::shared_ptr<employee_repository> repository) {
        _repository = std::move(repository);
    }

    [[get_mapping("/employees")]]
    std::vector<employee> all() {
        return _repository.find_all();
    }

    [[post_mapping("/employees")]]
    employee new_employee([[request_body]] employee new_employee) {
        return _repository.save(std::move(new_employee));
    }

    [[get_mapping("/employees/{id}")]]
    employee all([[path_variable]] size_t id) {
        std::optional<employee> e = _repository.find_by_id(id);
        if (e.has_value()) {
            return std::move(*e);
        }
        throw employee_not_found_exception(id);
    }

    [[delete_mapping("/employees/{id}")]]
    void delete_employee([[path_variable]] size_t id) {
        _repository.delete_by_id(id);
    }

private:
    std::shared_ptr<employee_repository> _repository;
};

и что это за магические слова? Что они должны означать? Зачем это?

Задача: сделать фреймворк, на котором удобно писать HTTP REST сервер.

Нужно, чтобы пользователь написал минимум кода, и при получении GET HTTP-запроса
host.com/employees/17
фрейморк бы вызвал метод
employee find(size_t id); со значением id=17
результат бы сериализовал в json и отдал клиенту

а при получении POST HTTP-запроса
host.com/employees
с телом json: {name: "john", age: 17}
вызвал бы метод
employee create_new_employee(employee new_employee);
и параметр new_employee создал бы из json-тела запроса.

Связывание входного URL с методом и параметрами должно происходить декларативно и с минимумом бойлерплейта. В C# и java эта задача решена, в C++ — нет.

типичный маппинг параметров на URL может быть таким, что URL
somedb.com/region-ru/discounts/?sort=date&page=6
преобразуется в вызов
std::vector<item> find_discounts(size_t start_page = 0, size_t count = 0, Sort sort = Sort::none);

с парамерами start_page = 6, sort = Sort::date, count = 0 (последнего нет в URL, остаётся значение по умолчанию).
на обычном С++ без всякой рефлексии
На костылях и особенностях компиляторов.
template <typename E>
constexpr auto n() noexcept {
  static_assert(is_enum_v<E>, "magic_enum::detail::n requires enum type.");
#if defined(MAGIC_ENUM_SUPPORTED) && MAGIC_ENUM_SUPPORTED
#  if defined(__clang__)
  constexpr string_view name{__PRETTY_FUNCTION__ + 34, sizeof(__PRETTY_FUNCTION__) - 36};
#  elif defined(__GNUC__)
  constexpr string_view name{__PRETTY_FUNCTION__ + 49, sizeof(__PRETTY_FUNCTION__) - 51};
#  elif defined(_MSC_VER)
  constexpr string_view name{__FUNCSIG__ + 40, sizeof(__FUNCSIG__) - 57};
#  endif
  return static_string<name.size()>{name};
#else
  return string_view{}; // Unsupported compiler.
#endif
}

Статья интересная. Спасибо.

Но, объективно, C++ уже не суждено стать языком общего назначения. Для Spring уже есть Spring Native. С этой самой рефлексией в нативном коде без таких сложностей.

Есть "забавный" самописный проект с++20 компилятора с рефлексией https://www.circle-lang.org/

По чему в комтете отвергают '@' ведь это ни с чем не спутаешь и сразу понятно о чём речь.

Фронту парсить @ проще чем нагромождение [:^:], я думаю это гораздо эффективне. А то всё это напоминает триграфы которые по моему ни кто не использовал и ни разу не видел.

По чему в комтете отвергают '@' ведь это ни с чем не спутаешь и сразу понятно о чём речь.
Наверное из-за того, что он в декорированных именах используется. Хотя и в таком случае проблемы не вижу.

Мне тоже не нравится, что не применяют давно обкатанные в других C-подобных языках практики и слишком сильно пекутся об обратной совместимости.
Похоже, что @meta что-то среднее между constexpr и template for.
Как бы получилось, что одно и то же можно записать разными способами.

Можно ли, используя эту рефлексию, реализовать примерно такое?

int a = 5;
print("a = {a}");

Чтобы функция вытащила строку между фигурными скобками, и использовала как переданную переменную a? Или как ссылку на локальную переменную из функции, из которой вызывают.

Это было бы достаточно жестко, если бы функция через рефлексию могла получить доступ к контексту, из которого ее вызывают. Как-то так:

template for (auto var : meta::calledFrom().variables())
{
    if (var.name == "a"){
        std::cout << [: var.getConstReference() :];
        break;
    }
}

Где бы вместо "a" мы взяли бы подстроку из фигурных скобок в "a = {a}". Это было бы очень мощно!

Можно ли, используя эту рефлексию, реализовать примерно такое?
Для этого не нужна рефлексия, обычно это отдельный механизм (см. f-strings и string interpolation).

Например, в C# это выглядит как $"a = {a}". Компилятор сам генерирует код для составления такой строки, никакая рефлексия для этого не нужна.

Можно было бы, конечно, при определённых возможностях рефлексии делать это и в рантайме, но если уж и делать такой механизм, то лучше в compile time.

f строки это то да, этим и навеянно. Просто мне стало интересно, возможно ли такое сделать этой ревлексией без специальных языковых конструкций

Кстати, если такой способ получения ссылок на переменные из вызываемой функции возможны, то с помощью перерузки строкового литерала (operator ""_f), можно сделать такие же f-строки. (только будет не префикс, а суффикс _f) И без узкоспециализированного синтаксиса только для формата. Чем и крут C++

Хотя, без синтаксической поддержки сложные выражения не подставишь, или нужен будет какой-то constexpr eval, что уже очень странно для c++. Не знаю, нужны ли вообще f-строки в этом языке, это другой вопрос. Мне интересно, можно ли вот так взять произвольную ссылку по имени на переменную из вызывающей функции

Мне интересно, можно ли вот так взять произвольную ссылку по имени на переменную из вызывающей функции
Компилятор имеет право удалить переменную, если она не нужна. А с этой фичей придётся вырубить все оптимизации, иначе нет гарантии, что в каком-то месте «рефлексия» не попросит ссылку на переменную.

В вашем же примере
int a = 5;
print("a = {a}");
компилятор обязан удалить неиспользуемую переменную a.

Рефлексия работает на времени компиляции, так что компилятор учтет, что используется ссылка, и либо соптимизирует внутри функции тоже, либо не будет оптимизировать

Оптимизация не является частью языка, и, если в языке есть переменная с именем a, то ее можно было бы брать из рефлексии, даже если она соптимизированна

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

С рантаймом вариант только явно указывать захватываемые переменные как в лямбдах, типа такого:
print([a], "a = {a}");

Какой тогда смысл оставлять что-то на рантайм?

Ну как, std::cout на compile time не получится ;)

С рантаймом вариант только явно указывать захватываемые переменные

Захват - это по суди передача по ссылке, при чем такая же явная. А интересно, может быть можно брать эти ссылки не передачей, а используя новый синтаксис. Понятно, что в рантайме этот адрес будет разный, но в компайлтайме кажется, что можно было бы указать, что этот адрес нужно получить

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

Так а как компилятор поймёт, какие переменные можно выкидывать из контекста, а какие нет? Надо либо явно их передавать, либо как-то помечать (по аналогии с volatile), либо парсить строку на этапе компиляции.

Он же не сможет проанализировать весь код, которому передаётся данный контекст. Это возможно сделать только в текущей единице трансляции или при линковке. Но на этапе линковки оптимизация уже произведена.
Так это уже обработка строки на этапе компиляции. Какой тогда смысл оставлять что-то на рантайм?
Так в статье выше обсуждается как раз рефлексия в compile-time. Примеры разбираются — кодогенерация json-сериализатора и получение имени enum.
А тут, как я понял, вопрос, можно ли реализовать string interpolation средствами этой compile-time рефлексии, не добавляя string interpolation напрямую в стандарт.
А тут, как я понял, вопрос, можно ли реализовать string interpolation средствами этой compile-time рефлексии
Ну не знаю, я пришёл к других выводам. По изложению мыслей не похоже, что подразумевался компайл-тайм. Особенно если взять примеры псевдокода и разговоры о ссылках и т.п. — на уровне компайл-тайма это избыточные понятия.

Не исключаю, что я неверно понял мысль, но меня не поправляли, когда я говорил о рантайме.
Судя по словам про суффикс _f, разбор строки в compile-time, и в print передаётся какой-то сложно-накрученный шаблон, в котором все переменные уже определены (а ф-ция print ходит по нему через template for, так в примере у комментатора).

Если же разбор делать в runtime, то у print должна быть сигнатура
void print(const char*);
и тогда эта строка вообще не известна на момент компиляции (может даже из внешнего файла читаться), и это ставит крест на
компилятор обязан удалить неиспользуемую переменную a.
так что компилятор учтет, что используется ссылка, и либо соптимизирует внутри функции тоже, либо не будет оптимизировать
Судя по словам про суффикс _f
Это же был просто отход от основной темы в сторону возможности реализации f-strings средствами языка (в compile time, разумеется):
Кстати, если такой способ получения ссылок на переменные из вызываемой функции возможны, то с помощью перерузки строкового литерала (operator ""_f), можно сделать такие же f-строки.

И далее возврат к основной теме:
Не знаю, нужны ли вообще f-строки в этом языке, это другой вопрос. Мне интересно, можно ли вот так взять произвольную ссылку по имени на переменную из вызывающей функции
Sign up to leave a comment.

Articles