#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
, поэтому это не так просто, как создать atag<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...
, в которые он поддерживает преобразование. Используйте описанный выше метод для сохранения этих функций преобразования в (глобальной) карте. (Обратите внимание, что эта карта может быть помещена в статическое хранилище, так как вы можете определить размер требуемого буфера и т. Д. Но использовать anstatic 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
список?