C Тип- стирание шаблона функции с использованием лямбд

#c #templates #c 14 #type-erasure

#c #шаблоны #c 14 #стирание типа

Вопрос:

Я пытаюсь ввести erase объект и столкнулся с небольшой проблемой, в которой, я надеюсь, кто-то здесь может иметь опыт.

У меня не было проблемы с удалением произвольных не шаблонных функций; до сих пор я занимался созданием пользовательской static коллекции указателей на функции в стиле «виртуальной таблицы». Все это управляется с помощью лямбд без захвата, поскольку они распадаются на указатели на свободные функции:

 template<typename Value, typename Key>
class VTable {
    Value (*)(const void*, const Keyamp;) at_function_ptr = nullptr;
    // ...

    template<typename T>
    static void build_vtable( VTable* table ) {
        // normalizes function into a simple 'Value (*)(const void*, const Keyamp;)'' type
        static const auto at_function = []( const void* p, const Keyamp; key ) {
            return static_cast<const T*>(p)->at(key);
        }
        // ...
        table->at_function_ptr =  at_function;
    }
    // ...
}
  

(Есть и другие вспомогательные функции / псевдонимы, которые для краткости опущены)

К сожалению, этот же подход не работает с функцией template .

Я хочу, чтобы класс с удаленным типом имел что-то похожее на следующее:

 template<typename U>
U convert( const void* ptr )
{
    return cast<U>( static_cast<const T*>( ptr ) );
}
  

где:

  • cast является ли свободная функция,
  • U преобразуется ли тип в,
  • T выполняется ли приведение к удаленному типу базового типа и
  • ptr является указателем со стиранием типа, который следует той же идиоме, приведенной выше для стирания типа.

[Редактировать: проблема выше заключается в том, что T она неизвестна из функции convert ; единственная функция, которая знает о T типе ‘s в примере build_vtable . Это может просто потребовать изменения дизайна]

Причина, по которой это стало сложной задачей, заключается в том, что, по-видимому, не существует простого способа ввода стирания обоих типов независимо. Классическая / идиоматическая техника стирания типа базового класса здесь не работает, поскольку у вас не может быть virtual template функции. Я экспериментировал с шаблоном, похожим на посетителя, с небольшим успехом по тем же причинам, что и выше.

Есть ли у кого-нибудь, у кого есть опыт в стирании типов, какие-либо предложения или методы, которые можно использовать для достижения того, что я пытаюсь сделать? Предпочтительно в коде, соответствующем стандартам c 14. Или, возможно, есть ли изменения в дизайне, которые могли бы способствовать реализации той же концепции, что и здесь?

Я уже некоторое время ищу этот ответ, и мне не очень повезло. Есть несколько случаев, похожих на то, что я пытаюсь сделать, но часто с достаточными различиями, так что решения, похоже, не применимы к одной и той же проблеме (пожалуйста, дайте мне знать, если я ошибаюсь!).

Похоже, что большинство чтений / блогов по этим темам, как правило, охватывают базовую технику стирания типов, но не то, что я ищу здесь!

Спасибо!

Примечание: пожалуйста, не рекомендуйте Boost. Я нахожусь в среде, где я не могу использовать их библиотеки, и не хочу вводить эту зависимость в кодовую базу.

Комментарии:

1. Не может cast быть функтором, а затем вызвать его cast(tag<U>{}, cast(tag<const T*>{}, ptr ) ) ?

2. @Jarod42 Проблема в том, что T это неизвестно изнутри convert , поэтому это не так просто, как создать a tag<const T*> в этот момент. Единственная функция в примере, которая знает тип T build_vtable

3. Во-первых, это не template функция; это функция template . Здесь важно понимать формулировку. Таким образом, это не фактическая функция. Фактическая функция такова convert<U> , как описывает Yakk. Поэтому вам понадобится convert_U_function_ptr внутри вашего VTable для каждого U типа

4. @zahir Хорошая формулировка, я на самом деле не думал об этом, когда набирал это. Спасибо за исправление! И теперь я убежден, что это тот подход, который мне нужен, я просто пытаюсь выяснить, как сделать это в общем случае для поддержки любого типа U , а не фиксированного набора типов.

Ответ №1:

Каждое отдельное convert<U> стирание является отдельным типом.

Вы можете ввести erase список таких функций, сохраняя метод выполнения этого в каждом случае. Итак, предположим, у вас есть Us... , введите erase all of convert<Us>... .

Если Us... коротко, это просто.

Если он длинный, это боль.

Возможно, что большинство из них может быть нулевым (поскольку в operation это незаконно), поэтому вы можете реализовать разреженную vtable, которая учитывает это, поэтому ваша vtable не будет большой и полной нулей. Это может быть сделано путем стирания типа функции (с использованием стандартной технологии vtable), которая возвращает ссылку (или средство доступа с удалением типа) на указанную разреженную vtable, которая сопоставляется с std::typeindex преобразователем U-placement-constructor (который записывает в a void* в подписи). Затем вы запускаете эту функцию, извлекаете запись, создаете буфер для хранения U, вызываете конвертер U-placement-constructor, передающий в этот буфер.

Все это происходит в вашей type_erased_convert<U> функции (которая сама по себе не стирается), поэтому конечным пользователям не нужно заботиться о внутренних деталях.

Вы знаете, просто.

Ограничение заключается в том, что список возможных поддерживаемых типов преобразования должен U располагаться до местоположения стирания типа. Лично я бы ограничился type_erased_convert<U> вызовом только одного списка типов U и согласился с тем, что этот список должен быть принципиально коротким.


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

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


 std::function< void(void const*, void*) > constructor;

std::function< constructor( std::typeindex ) > ctor_map;

template<class...Us>
struct type_list {};

using target_types = type_list<int, double, std::string>;

template<class T, class U>
constructor do_convert( std::false_type ) { return {}; }
template<class T, class U>
constructor do_convert( std::true_type ) {
  return []( void const* tin, void* uout ) {
    new(uout) U(cast<U>( static_cast<const T*>( ptr ) ));
  };
}

template<class T, class...Us>
ctor_map get_ctor_map(std::type_list<Us...>) {
  std::unordered_map< std::typeindex, constructor > retval;
  using discard = int[];
  (void)discard{0,(void(
    can_convert<U(T)>{}?
      (retval[typeid(U)] = do_convert<T,U>( can_convert<U(T)>{} )),0
    : 0
  ),0)...};
  return [retval]( std::typeindex index ) {
    auto it = retval.find(index);
    if (it == retval.end()) return {};
    return it->second;
  };
}

template<class T>
ctor_map get_ctor_map() {
  return get_ctor_map<T>(target_types);
}
  

Вы можете unordered_map заменить шаблон на основе компактного стека, если он маленький. Обратите внимание, что std::function в MSVC ограничено примерно 64 байтами или около того?


Если вам не нужен фиксированный список типов source / dest, мы можем отделить это.

  • Предоставьте typeindex информацию о типе, хранящемся в контейнере для удаления типов, и возможность получить доступ к void const* тому, что указывает на него.

  • Создайте признак типа, который сопоставляет тип T со списком типов Us... , в которые он поддерживает преобразование. Используйте описанный выше метод для сохранения этих функций преобразования в (глобальной) карте. (Обратите внимание, что эта карта может быть помещена в статическое хранилище, так как вы можете определить размер требуемого буфера и т. Д. Но использовать an static unordered_map проще).

  • Создайте второй признак типа, который сопоставляет тип U со списком типов Ts... , из которых он поддерживает преобразование.

  • В обоих случаях convert_construct( T const* src, tag_t<U>, void* dest ) вызывается функция для выполнения фактического преобразования.

Вы бы начали с набора универсальных целей type_list<int, std::string, whatever> . Определенный тип будет дополнять его новым списком.

Для типа T , создающего свою разреженную таблицу преобразования, мы бы попробовали каждый целевой тип. Если перегрузка convert_construct не может быть найдена, карта не будет заполнена для этого случая. (Генерация ошибок во время компиляции для типов, явно добавленных для работы, T является опцией).

С другой стороны, когда мы вызываем type_erased_convert_to<U>( from ) , мы ищем другую таблицу, которая сопоставляет U пересечение типов typeindex U(*)(void const* src) преобразователю. T Для поиска конвертера используются как исходная карта, полученная из стираемого типа, так T и U полученная в коде переноса.

Теперь это не разрешает определенные виды преобразования. Например, тип T , который преобразует-из чего-либо с помощью .data() -> U* .size() -> size_t метода and, должен явно указывать каждый тип, из которого он преобразуется.

Следующим шагом было бы разрешить многоступенчатое преобразование. Многоступенчатое преобразование — это когда вы учите T преобразовывать — в некоторые (набор) известных типов, а мы учим U преобразовывать — из аналогичного (набора) известных типов. (Я признаю, что известность этих типов необязательна; все, что вам нужно знать, это как их создавать и уничтожать, какое хранилище вам нужно, и способ сопоставления параметров «туда T и U обратно», чтобы использовать их в качестве посредника.)

Это может показаться чрезмерным. Но примером этого является возможность преобразования std::int64_t и преобразования из него в любой целочисленный тип со знаком (а также аналогично для uint64_t и без знака).

Или возможность конвертировать-в словарь пар ключ-значение, а затем изучить этот словарь с другой стороны, чтобы определить, можем ли мы преобразовать- из него.

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

Комментарии:

1. Хотя это отличное предложение, я не уверен, применимо ли это к текущей проблеме. Я внес небольшую правку, чтобы подчеркнуть проблему, с которой я столкнулся; это то, что функция convert ничего не знает T . Единственная функция в приведенном выше примере, которая выполняет это build_vtable , в противном случае тип просто void* .

2. Хотя я чувствую, что это менее вероятно, я изначально надеялся выполнить это, не требуя выделения кучи (например, с помощью map). Хотя я полагаю, поскольку это статично, это можно было бы сделать с помощью навязчивого списка пар ( std::type_index ,function ptr) .

3. @Bitwize Да, type_erased_convert не знает о T в моем приведенном выше предложении. Операция удаления типа (void*)-> — это сопоставление с typeindex std::function<void(U*)> разреженной картой, не обязательно выделять кучу.

4. @Bitwize сбросил некоторый код. Не скомпилирован. Вы бы ввели erase get_ctor_map , используя свой механизм, возможно, оптимизированный для возврата static значения или ссылки на static значение. Вы бы предпочли type_erased_convert использовать get_ctor_map erased, найдите его U на карте, если он есть, вызовите функцию, чтобы получить возвращаемое значение. Если его там нет, это будет exit(-1) или выброс или что-то в этом роде. Или это вернет optional<U> и допустит сбой как возможность.

5. Спасибо за пример, это намного упрощает понимание! Это блестящий подход, но у меня есть один вопрос по этому поводу. Если я не ошибаюсь, этот пример, похоже, работает только с конечным набором типов — когда я ищу поддержку любого произвольного типа U . Знаете ли вы какой-либо способ, которым это можно было бы расширить для поддержки любого произвольного типа, чтобы для него не требовался target_types список?