C++使用lambdas对函数模板进行类型擦除

C++ Type-erasure of a function template using lambdas

本文关键字:类型 擦除 函数模板 使用 lambdas C++      更新时间:2023-10-16

我试图键入擦除对象,遇到了一个问题,我希望这里的人可能有专业知识。

我从来没有遇到过擦除任意非模板函数的问题;到目前为止,我一直在做的是创建一个自定义的static"虚拟表"式的函数指针集合。这一切都是通过非捕获Lambda来管理的,因为它们衰减为自由函数指针:

template<typename Value, typename Key>
class VTable {
    Value (*)(const void*, const Key&) at_function_ptr = nullptr;
    // ...
    template<typename T>
    static void build_vtable( VTable* table ) {
        // normalizes function into a simple 'Value (*)(const void*, const Key&)'' type
        static const auto at_function = []( const void* p, const Key& 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 ) );
}

其中:

  • CCD_ 3是自由函数
  • CCD_ 4是被广播到的类型
  • T是从中广播的底层类型擦除类型,并且
  • ptr是类型擦除指针,遵循与上面的类型擦除相同的习惯用法

[编辑:上面的问题是从函数convert中不知道T;本例中唯一知道T类型的函数是build_vtable。这可能只需要更改设计]

这变得具有挑战性的原因是,似乎没有任何简单的方法来键入擦除这两种类型独立地。基类的经典/惯用类型擦除技术在这里不起作用,因为您不能11CCD_ 12功能。我尝试过类似访问者的模式,但在类似的模式中收效甚微以上原因。

有打字擦除经验的人有什么建议或技术可以用来实现我想做什么?最好采用符合c++14规范的标准。或者,也许有没有设计上的改变可以促进这里想要的相同概念?

我已经四处寻找这个答案有一段时间了,运气不太好。有一些案例与我试图做的类似,但往往有足够的差异,以至于解决方案似乎不适用于同一个问题(如果我错了,请告诉我!(。

似乎大多数关于这些主题的阅读/博客都倾向于涵盖基本类型的擦除技术,但不是我在这里寻找的!

谢谢!

注意:请不要推荐Boost。我所处的环境无法使用他们的图书馆希望将这种依赖性引入到代码库中

每个不同的convert<U>都是不同类型的擦除。

您可以键入擦除此类函数的列表,存储在每种情况下执行该操作的方法。所以假设您有Us...,键入擦除所有convert<Us>...

如果Us...很短,这很容易。

如果它很长,这是一种痛苦。

其中大多数可能是空的(因为在操作中是非法的(,所以你可以实现一个考虑到这一点的稀疏vtable,这样你的vtable就不会很大,也不会满是零。这可以通过类型擦除函数(使用标准vtable技术(来实现,该函数返回对所述稀疏vtable的引用(或类型擦除访问器(,该稀疏vtable从std::typeindex映射到U-placement-constructor转换器(写入签名中的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很小时,您可以将其替换为紧凑的基于堆栈的CCD_23。请注意,MSVC中的std::function限制在大约64个字节左右?


如果您不想要一个固定的源/目标类型列表,我们可以将其解耦。

  • 暴露存储在类型擦除容器中的类型的typeindex,以及获取指向它的void const*的能力。

  • 创建一个类型trait,将类型T映射到它支持转换的类型Us...的列表。使用上述技术将这些转换函数存储在(全局(映射中。(请注意,此映射可以放置在静态存储中,因为您可以推断所需缓冲区的大小等。但使用static unordered_map更容易(。

  • 创建第二个类型trait,将类型cast0映射到支持转换的类型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 )时,我们会查找一个不同的表,该表将类型Utypeindex映射到U0转换器。参考从类型擦除的T得到的from-T映射和包装代码中得到的to-U来找到转换器。

现在,这不允许某些类型的转换。例如,使用.data() -> U*.size() -> size_t方法从任何类型转换的类型T需要显式列出它从中转换的每个类型。

下一步将是允许进行多步骤转换。多步骤转换是指教T转换为某些(一组(著名类型,我们教U从类似的(一套(著名类型转换。(我承认,这些类型的名声是可选的;你只需要知道如何创建和销毁它们,你需要什么存储,以及如何匹配T-to和U-from选项,将它们用作中介。(

这可能看起来设计过度了。但转换为std::int64_t并从其转换为任何有符号积分类型的能力就是一个例子(类似于uint64_t和无符号(。

或者能够转换为键值对的字典,然后在另一边检查这个字典,以确定我们是否可以从中转换。

当你沿着这条路走下去时,你会想要检查各种脚本和字节码语言中的松散类型系统,以了解它们是如何做到的