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

Hum*_*ler 8 c++ templates type-erasure c++14

我正在尝试键入擦除一个对象并遇到一个问题,我希望有人在这里可能有专业知识.

我没有遇到任何类型的问题 - 擦除任意非模板化函数; 到目前为止,我一直在做的是创建一个自定义的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;
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

(为简洁起见,省略了更多辅助函数/别名)

遗憾的是,这种方法与功能无关template.

我希望类型擦除类具有类似于以下内容的东西:

template<typename U>
U convert( const void* ptr )
{
    return cast<U>( static_cast<const T*>( ptr ) );
}
Run Code Online (Sandbox Code Playgroud)

哪里:

  • cast 是一个免费的功能,
  • U 是被铸造的类型,
  • T 是从中铸造的基础类型擦除类型,和
  • ptr 是类型擦除指针,它遵循上面用于类型擦除的相同习语.

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

这变得具有挑战性的原因是,似乎没有任何简单的方法可以独立地键入两种类型.基类的经典/惯用类型擦除技术在这里不起作用,因为你不能有一个virtual template功能.由于与上述类似的原因,我尝试了类似访问者的模式,但收效甚微.

有类型擦除经验的人是否有任何建议或技术可用于实现我想要做的事情?最好是符合标准的c ++ 14代码.或者,也许是否有设计变更可能会促进这里所需的相同概念?

我一直在寻找这个答案一段时间,并没有太多运气.有一些情况类似于我正在尝试做的事情,但往往有足够的差异,解决方案似乎不适用于同样的问题(如果我错了,请告诉我!).

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

谢谢!

注意:请不要推荐Boost.我在一个我无法使用他们的库的环境中,并且不希望将该依赖项引入代码库.

Yak*_*ont 5

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

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

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

如果它很长,这是一个痛苦.

它们中的大多数可能为空(因为在操作中是非法的),因此您可以实现将此考虑在内的稀疏vtable,因此您的vtable不大且充满零.这可以通过类型擦除函数(使用标准vtable技术)来完成,该函数将引用(或类型擦除的访问器)返回到映射std::typeindex到U-placement-constructor转换器的稀疏vtable (写入void*签名中的a) ).然后运行该函数,提取条目,创建缓冲区以存储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);
}
Run Code Online (Sandbox Code Playgroud)

unordered_map当它很小时,您可以用基于堆栈的紧凑型替换它.请注意,std::function在MSVC中大约限制在64字节左右?


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

  • 公开typeindex存储在类型擦除容器中的类型,以及获取void const*该点的能力.

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

  • 创建第二个类型特征,将类型映射UTs...它支持转换的类型列表.

  • 在这两种情况下,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 ),我们会寻找一个将类型交叉映射到转换器的不同表.从类型擦除得到的地图和包装代码中的结果都可以查找转换器.UtypeindexU(*)(void const* src)TTU

现在,这不允许某些类型的转换.例如,T从具有.data() -> U*.size() -> size_t方法的任何东西转换的类型需要明确地列出它转换的每个类型.

下一步是承认多步转换.一个多步骤转换是教你T转换为一些(一组)着名类型的地方,我们教导U转换来自类似(一组)着名类型.(这些类型的名声是可选的,我承认,所有你需要知道的是如何创建和销毁都需要,哪些存储和办法投其所好T-to和U-from选项,用它们作为中介.)

这可能看起来过于设计.但转换为转换std::int64_t并转换为任何有符号整数类型的能力就是这样的一个例子(同样适用于uint64_t和未签名).

或者转换为键值对字典的能力,然后在另一侧检查这个字典以确定我们是否可以转换它.

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