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

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

我正在尝试键入擦除一个对象并 运行 解决一个问题,我希望这里有人可能有这方面的专业知识。

我在类型擦除任意非模板函数时没有遇到问题;到目前为止,我一直在做的是创建自定义 static "virtual table"-esque 函数指针集合。这一切都是通过非捕获 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;
    }
    // ...
}

(还有更多的帮手functions/aliases为了简洁省略)

遗憾的是,同样的方法不适用于函数 template

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

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

其中:

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

这变得具有挑战性的原因是似乎没有任何简单的方法来键入擦除两种类型 独立地。 base-class 的 classical/idiomatic 类型擦除技术在这里不起作用,因为你不能 一个 virtual template 函数。我已经尝试过类似访问者的模式,但对于类似的情况收效甚微 以上原因。

有没有类型擦除经验的人有任何建议或技术可以用来实现什么? 我在努力做什么?最好使用符合标准的 c++14 代码。 或者,也许有设计更改可以促进此处所需的相同概念?

我一直在寻找这个答案有一段时间了,但运气不佳。有一些案例与我正在尝试做的相似,但通常有足够的差异,以至于解决方案似乎不适用于同一问题(如果我错了,请告诉我!)。

这些主题中的大多数 readings/blogs 似乎都涵盖了基本的类型擦除技术,但不是我在这里寻找的内容!

谢谢!

注意:请不要推荐Boost。我处于无法使用他们的库的环境中,并且不 希望将这种依赖性引入代码库。

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

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

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

如果它很长,这很痛苦。

其中大部分可能为空(因为在操作中是非法的),因此您可以实现考虑到这一点的稀疏 vtable,因此您的 vtable不是很大而且全是零。这可以通过类型擦除函数(使用标准 vtable 技术)来完成,该函数 returns 对从 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小的时候,你可以用一个紧凑的基于堆栈的替换。请注意,MSVC 中的 std::function 被限制为大约 64 个字节左右?


如果您不想要 source/dest 类型的固定列表,我们可以将其解耦。

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

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

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

  • 在这两种情况下,都会调用函数 convert_construct( T const* src, tag_t<U>, void* dest ) 来进行实际转换。

您将从一组通用目标开始 type_list<int, std::string, whatever>。特定类型将通过拥有新列表来扩充它。

对于类型 T 构建其稀疏转换 table 我们将尝试每个目标类型。如果找不到 convert_construct 的重载,则不会为这种情况填充地图。 (为显式添加以与 T 一起使用的类型生成编译时错误是一个选项)。

在另一端,当我们调用 type_erased_convert_to<U>( from ) 时,我们寻找映射类型 Udifferent table typeindexU(*)(void const* src) 转换器。从类型擦除的 T 中获得的 from-T 映射和包装代码中获得的 to-U 映射都被查询以找到转换器。

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

下一步将是承认多步转换。多步转换是您教 T 转换为一些(一组)著名类型,而我们教 U 从相似(一组)著名类型转换。 (这些类型的知名度是可选的,我承认;你只需要知道如何创建和销毁它们,你需要什么存储,以及匹配 T-to 和 U-来自选项,将它们用作中介。)

这似乎设计过度了。但是转换为 std::int64_t 并将其转换为任何带符号整数类型的能力就是一个例子(对于 uint64_t 和无符号类型也是如此)。

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

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