Madokakaroto (◉3◉) is writing cpp

magic_get - A reflection techniques using modern C++

magic_get是一个很有趣的C++反射库,它不需要让使用者显式地做额外的事情,但是限制是反射的类型必须是Aggregate Initializable. 在它出现之前,我们碰到需要反射这一类问题都是怎么处理的呢?

1. 传统的反射标记: 侵入式与非侵入式

对于标准输入输出流,我们要重载operator « 和 ».

namespace client
{
    struct foo 
    {
        std::string     name;
        int             age;
    };

    inline std::ostream& operator<<(std::ostream& os, foo const& f) 
    {
        return os << f.name << " " << f.age;
    }

    inline std::istream& operator>>(std::instream& is, foo& f)
    {
        return os >> f.name >> f.age;
    }
}

重载的«和»操作符与struct foo都在同一个命名空间下,并且使用operator«和»都会使用非限定名字的查找规则,这里ADL查找会起作用,无论在任何命名空间下都能帮我们找到正确的重载操作符。但是每个类都要这样手动实现,未免显得太啰嗦。于是,不少类库都各自提供了工具,标记想要序列化的成员,从而让开发者从写死地访问某个确定成员的方式,变成遍历或者通过索引来访问成员的方式。

这里有两个选择,一个是像msgpack的侵入式标记:

namespace client
{
    struct foo
    {
        std::string     name;
        int             age;
        MSGPACK_DEFINE(name, age)
    };
}

还有一种是类似boost.fusion的非侵入式标记:

namespace client
{
    struct foo
    {
        std::string     name;
        int             age;
    };
}

// has to be in the global scope
BOOST_FUSION_ADATP_STRUCT(
    client::foo,
    (std::string, name)
    (int, age)
)

由于boost.fusion是使用类模板特化实现的,所以标记的代码不得不写在全局的namespace中。而iguana的某一个版本,使用函数模板返回local class的trick,可以让开发者把宏写在定义struct相同的命名空间中:

namespace client
{
    struct foo
    {
        std::string     name;
        int             age;
    };

    REFLECTION(foo, name, age);
}

综合来看,侵入式和非侵入式都各有优势。笔者认为侵入式最大的优势就是可以很好的处理私有变量;而非侵入式最大优势是可以处理第三方代码或者开发者无法修改源码的结构化数据。这些技巧,在C++远古时代应该就已经被熟知。

而cppcon2016中的magic_get不需要做任何显式地标记工作,就能达成反射成员的目标,非常惊艳。

#include <iostream>
#include <string>
#include "boost/pfr/precise.hpp"

struct foo 
{
    std::string     name;
    int             age;
};

int main() 
{
    foo f{"Madoka", 14};
    std::cout 
        << boost::pfr::get<0>(f)
        << boost::pfr::get<1>(f);
    return 0;
}

magic_get解决问题的方式,是thinking in modern C++教科书式的范例,有很高的价值和意义,下文会详细讨论magic_get中的技术要点。

2. C++目前所拥有的设施

先抛出一个基本的结论,截止目前C++17所拥有的语言基础设施,还不可能反射struct foo而不做任何额外的事情。目前我们拥有:

  1. SFINAE
  2. Meta-programming with
    • templates
    • constexpr
  3. Static dispatch with
    • function overload
    • class template (partial) specialization
    • constexpr if
  4. Unevaluated operators

这些设施能完成的事情仅仅只能test, 例如我们只能test两个类型的继承关系,但不能获取一个类型的基类:

std::is_base_of_v<foo, bar>;

// ERROR! No way to achieve like the way below
using type = std::base_type_t<foo>; 

面对成员变量,由于expression SFINAE是通过显式推导表达式的返回类型来完成的,所以我们必须要写出表达式:

template <typename T, typename = void>
struct has_member_name : std::false_type {};
template <typename T>
struct has_member_name<T, std::void_t<decltype(std::declval<T>().name)>>
    : std::true_type {};

std::has_member_name<T>::value;

现实的情况是,成员变量的符号有无穷种组合,而我们无法列举出所有这样的组合。test的方式并不能直接处理这样的问题,那magic_get是如何做到的?答案就是SEARCH & TEST

3. Aggregate Initialization

在剖析magic_get之前,先简单介绍一下在magic_get种使用的test的方式:Aggregate Initialization. 具体定义请参阅链接。

还是以struct foo为例,我们可以这样初始化

struct foo
{
    std::string     name;
    int             age;
};

foo f = { "madoka", 14 };

用于初始化struct foo的参数列表的数目可以少于成员变量的数目,但是不能多于:

foo f0 = {};                                     // OK
foo f1 = { "madoka" };                           // OK
foo f2 = { "kakaroto", 50, "super saiyajin" };   // ERROR!

如果我们定义一种类型,它能够cast到任意类型,会发生什么?

struct universe_type
{
    size_t ignored;

    template <typename T>
    constexpr operator T() const noexcept;      // only declaration
};

我们可以利用Aggregate Initialization与universe_type这样组合使用:

using type = decltype(foo{ universe_type{0}, universe_type{1} });

由于universe_type可以cast到任意类型,但是case的操作符只给出了申明而没有给出定义,所以要使用unevaluated operator让aggregate initialization的表达式仅存在于编译期,能够推到出foo类型,就证明表达式is well-formed.

以上就是magic_get反射功能的key point,它利用aggregate initialization搜索成员变量的数目,并且在universe_type隐式类型转换为成员变量类型的时候,记录下cast的类型并与传入的index相绑定。magic_get考虑的很多因素,诸如copy和move构造,还有嵌套的struct等,magic_get实际的实现代码与鄙文所列的部分实现还是有出入的。所以在理解完magic_get的实现思路之后,不妨去阅读以下magic_get的源码。源码比较简短,但有着不浅的奥义。

接下来详细展开三个问题:

  • 如何SEARCH成员变量的数目;
  • 如何获取成员变量的类型;
  • 如何访问成员变量。

4. 如何获取成员变量的数目

由于aggregate initialization对于实参列表的数目要求是小于和等于类型成员变量的数目,所以搜索准确的成员变量数目变得麻烦了不少。magic_get的做法是,确定一个成员变量数目的搜索上限,再确定一个搜索下限,然后进行二分查找。

搜索下限很简单,就是ZERO. 而搜索上限magic_get设置的是sizeof(foo) * BITS_PER_BYTE, 也就是sizeof(foo) * 8. 原因是存在如下使用位域的极端情况:

struct fee
{
    int a1 : 1;
    int a2 : 1;
    ...
};

搜索的接口如下:

// helper placehodler for bounds
template<size_t N>
using size_t_ = std::integral_constant<size_t, N>;

// LB is short for LowerBound, while UB for UpperBound
template <typename T, size_t LB, size_t UB>
constexpr size_t detect_fields_count(size_t_<LB> lb, size_t_<UB> ub) noexcept; 

利用第三节所提到的test方法,magic_get中有一个使用模板工具,来测试T是否可以使用N个universe_type来进行aggregate initialization:

template <typename T, typename indics, typename = void>
struct is_aggreate_initializable_impl : std::false_type {};
template <typename T, size_t ... N>
struct is_aggreate_initializable_impl<T,
    std::index_sequence<N...>,
    std::void_t<decltype(T{ universe_type{N}... })>
> : std::true_type {};

template <typename T, size_t N>
struct is_aggreate_initializable : is_aggreate_initializable_impl<
    T, std::make_index_sequence<N>
> {};

这个时候我们把SFINAE的机制用在detect_fields_count接口。test失败的情况,证明UpperBound的数目太大,我们要向下搜索;而test成功的情况也并不能确定成员变量的数目,我们还需向上搜索。

向下搜索的策略,就是一个二分策略。把UpperBound缩小至UpperBound与LowerBound均值:

template <typename T, size_t LB, size_t UB>
constexpr auto detect_fields_count(size_t_<LB>, size_t_<UB>) noexcept
    -> disable_if_t<is_aggreate_initializable<T, UB>::value, size_t>
{
    using next_ub_t = size_t_<(LB + UB) / 2>;
    return detect_fields_count<T>(size_t_<LB>{}, next_ub_t{});
}

向上搜索的时候,可以断定UpperBound可以作为新的LowerBound,而UpperBound更新为相差的一半:

template <typename T, size_t LB, size_t UB>
constexpr auto detect_fields_count(size_t_<LB>, size_t_<UB>) noexcept
    -> std::enable_if_t<is_aggreate_initializable<T, UB>::value, size_t>
{
    using next_ub_t = size_t_<(UB - LB + 1) / 2>;
    return detect_fields_count<T>(size_t_<UB>{}, next_ub_t{});
}

搜索会收敛到UpperBound与LowerBound相等, 因为LowerBound始终都是可以满足test的情况。

template <typename T, size_t N>
constexpr size_t detect_fields_count(size_t_<N>, size_t_<N>) noexcept
{
    return N;
}

以struct foo为例,模拟一下整个计算的过程:

0: detect_fields_count<0, 256>  - Test Failed
1: detect_fields_count<0, 128>  - Test Faield
2: detect_fields_count<0, 64>   - Test Faield
3: detect_fields_count<0, 32>   - Test Faield
4: detect_fields_count<0, 16>   - Test Faield
5: detect_fields_count<0, 8>    - Test Faield
6: detect_fields_count<0, 4>    - Test Faield
7: detect_fields_count<0, 2>    - Test Succeed!
8: detect_fields_count<2, 3>    - Test Faield
9: detect_fields_count<2, 2>    - Terminated! 

至此,我们就通过Search & Test获取到了struct foo的成员变量数目为2.

5. 如何获取成员变量的类型

如果获取成员变量的类型,这个问题到现在来看并不好解决,应该换一种思维模式。首先我们再次回顾一下is_aggreate_initializable这个boolean模板元函数的实现:

template <typename T, size_t ... I>
struct is_aggreate_initializable_impl<T,
    std::index_sequence<N...>,
    std::void_t<decltype(T{ universe_type{I}... })>
> : std::true_type {};

这里我们使用了index sequence来展开出N个universe_type的构造,并且每个universe_type传递的I的值是从0至N-1,这也是为什么universe_type要添加一个size_t的成员变量的原因。

接着本节最开始的那个话题,如何获取成员变量的类型并不好解决,这个问题就目前我们所拥有的条件信息可以替换为:如何记录第i个universe_type在隐式类型转换到对应成员变量类型时的信息。也就我们要构造这样的一个映射:

map: <i, T> => i-th data member type, where i is from [0, N - 1]

目前的C++能够完成这一项工作吗?笔者在阅读magic_get的代码的时候发现了代码注释中的一篇博文: type-loophole. 博文中给出了一段简单的代码:

template<int N> struct tag{};

template<typename T, int N>
struct loophole_t {
  friend auto loophole(tag<N>) { return T{}; };
};

auto loophole(tag<0>);

sizeof(loophole_t<std::string, 0>);

static_assert(std::is_same<std::string, decltype( loophole(tag<0>{}) ) >::value);

需要注意的是类模板中loophole_t中friend函数的定义式,并不是loophole_t的成员函数,而是与loophole_t在同一个命名空间下的函数,只不过该函数是loophole_t的友元,并且该友元函数同loophole_t一同实例化。具体可以参阅《C++ Templates, 2nd》的2.4节,12.5节也详细讨论了模板与友元的很多细节,例如何时可以写同本例一样的友元定义式等。《C++ Template, 1st》也有完整的讨论,可以参阅对应的章节。

在使用sizeof这个unevaluated operator的时候,代码促使loophole_t<std::string, 0>发生实例化,一同实例化的还有auto loophole(tag<0>)函数,最后使得该函数的返回值可以推导为std::string. 是的,你没有看错,这个是带状态的元编程方法, stateful metaprogramming.

C++之父在之前的访谈中提到过,初衷是把template设计成基于lambda演算的子系统,但是实现以后却变成了一个完备的图灵机。也就是说,模板元编程是可以有状态的。笔者在知乎上看到过的一篇,使用stateful metaprogramming来实现的编译期的计数器,请参阅这里

把这个trick应用到magic_get中需要调整一下。tag除了绑定index N之外,还需要绑定我们需要反射的类型。其次,肯定不能手动写sizeof(loophole_t<std::string, 0>)这样的显式类型的代码,需要调整为配合universe_type隐式类型转换的代码。所以,笔者提取了要点功能把代码抽出,具体代码可以参阅这里。最后装配实施到类库中的详细方法,请参阅magic_get的具体实现。

该通过友元函数来达到带状态的模板元编程方法的机制,被标准委员会一致地认为是不应该支持的,但目前还没有给出一个确切的方案来禁止它,所以在未来的某个时间点,这个机制会被禁止。我们的msvc表现出色,并不支持type-loophole ^_^. 那在msvc中如何解决这个问题呢?幸好C++17有structured bindings.

template <typename T>
constexpr auto tie_as_tuple(T& val, size_t_<2>)
{
    auto& [a, b] = val;
    // ....
}

接下来,我们就可以通过a和b来推导成员变量的类型了。这里又充分体现出了表达式的局限性,也就是绑定identifiers是显式的。不过我们的大神不仅聪明,而且勤奋,他自动生成了size从1到100的实现。

6. 如何访问成员变量

成员变量的访问,在解决了成员变量的数目和各个成员变量的类型两个问题后,变得简单了不少。magic_get中针对type-loophole和structured bindings两种方案的实现也是略有不同。成员访问的思路很简单,但是实现的代码比较啰嗦。因篇幅所限,鄙文就仅在此简述实现的思路,详细的事情可以参阅magic_get的具体实现。

type-loophole的方案较为复杂,在获取了成员变量的数目和类型后,需要构造一个tuple,将成员变量的类型按照顺序平铺进来。然后利用metaprogramming计算出每个成员在反射类型中的内存对齐的信息。这样就构造出了一个大小和内存对齐属性与反射类型一模一样的tuple.访问成员变量就可以通过tuple查询成员变量相对于结构体头部的偏移量。

这里需要提及的是,在搜索成员变量数目的时候,作者考虑的位域影响成员变量数目的情况,但是在访问成员的变量的实现中只使用了每个成员变量类型各自的对齐属性。也就是说,magic_get至此每个成员变量的类型指定各自的align属性,但是不支持使用了位域的结构体。

而structured bindings的方案就特别简单了。既然我们已经通过表达式获取的每个成员的左值引用,我们就可以直接把引用forward成一个引用的tuple,通过正常的tuple访问接口就能够访问到反射类型的成员了。

8. 结论

  1. 当前的C++语言基础设施,还无法提供一个完备的反射方案,反射的功能需要我们写额外的标记代码,或者在有限的条件下使用;
  2. magic_get只能使用在aggregate initializible的类型上, 这个限制对于生产环境中C++的使用是无法忽略的;
  3. magic_get在C++14标准下使用的type-loophole机制将在未来的某个C++版本中禁止;
  4. magic_get使用的Serach & Test的方法和思路对我们使用modern C++解决实际问题时有不小的启发;
  5. 期待C++ Reflection标准的正式到来。

vulkan api的学习与实践 - 类型安全的接口封装

1. 前言

Vulkan作为OpenGL的继任者,解决了不少历史问题。例如Direct State Access取代State Machine,Validation Layers提供调式切面,轻量级的驱动并提供Core API和扩展等等。文本将浅讨vulkan loader,并结合modern C++设计和实践一个类型安全的接口封装方案。

2. Vulkan Loader

Vulkan是分层设计的API,并且还支持多个GPU和多个图形驱动程序。Vulkan Loader的一个重要的作用,就是把用户对Vulkan API的调用分发到不同的ICDs(Install CLient Drivers).

Vulkan Loader是根据Object Model作为context来决定如何分发的。Explicit是Vulkan API的设计原则之一,所有的Vulkan API接口的第一个参数,都是Object Model的对象,例如Device,Queue和CommandBuffer等。对用户层,Vulkan提供Handler来索引这些对象,而内部则是一个指向结构体的指针,这个指针就包含了一个分发表,指向适用于该Vulkan Object的函数指针。

Vulkan Loader包含在Vulkan的SDK里面。在windows平台下名为vulkan-1.dll,当然不同的平台,还有API版本的演进这个名字会有所不同。使用Vulkan Loader的方式有两种:

  1. 直接使用vulkan.h已经定义好的函数prototypes,静态地加载Vulkan Loader
  2. 使用宏VK_NO_PROTOTYPES来禁用vulkan.h中的函数prototypes,并使用vkGetInstanceProcAddr或者vkGetDeviceProcAddr手动加载Vulkan Loader中的函数

第一种方法使用起来很简单,但是会有额外的运行期开销。由于静态加载地函数是全局的函数,并没有与特定的Object关联,所以全局函数还需要做一次间接地重定向。第二种方式虽然看似复杂,但是我们是对Object显式地加载API的函数,能够消除这个重定向。并且第二种方法更符合Vulkan的架构设计。

3. Global,Instance & Device 层级的API函数

Vulkan API有三个层级的API接口

  • Global层级
  • Instance层级
  • Device层级

Global层级的接口主要负责创建Vulkan Instance实例,查询Layers和Extentions的一些信息。Instance层级的接口主要负责Device的创建,包括Physical Device的枚举,提供属性,特性和扩展的数据查询还有创建Logical Device。而Device层级的接口,负责处理所有的Object Model.

Global和Instance层级的接口,都要通过vkGetInstanceProcAddr加载,不同是Instance层级需要制定VkInstance的Handler. 而vkGetInstanceProcAddr这个函数我们可以通过boost::dll来导出。

boost::dll::shared_library shared_lib { 
    "vulkan-1.dll", boost::dll::load_mode::search_system_folders };
shared_lib.get<std::remove_pointer_t<PFN_vkGetInstanceProcAddr>>(
    "vkGetInstanceProcAddr"
);

Device层级的接口通过vkGetDeviceProcAddr来获取,而这个函数本身是通过vkGetInstanceProcAddr函数获得的。

因此,在实践第二种方法,Instance和Device的实例关系会以如下结构呈现。

graph TD
    G[Global]
    G --> A1(Instance)
    G --> A2(Instance)
    A1 --> B1[Physical Device]
    B1 --> C1(Logical Device) 
    A2 --> B21[Physical Device]
    A2 --> B22[Physical Device]
    B21 --> C2(Logical Device) 
    B22 --> C21(Logical Device)
    B22 --> C22(Logical Device)

可以看出Vulkan的设计确实是完美兼顾了多卡多驱动。但是它并没有以直接的形式展现出这种模式,而是以Instance,Physical Device和Logical Device的这种实例层级关系来表达。虽然Vulkan的设计如是,但是在绝大多数的工程实践中,仅仅只会用最左边子树的模式来使用Vulkan.

而vulkan.h中的函数prototypes不再是全局函数,而是Global,Instance和Device实例的成员函数。

4. 扩展与接口生成

Vulkan的core API比OpenGL小了很多,甚至不包含任何present的接口。而平台相关的功能,和各大硬件或驱动厂商独特的功能都是以扩展的形式,按照统一的形式提供给用户使用。例如使用core和KHR扩展以及Win32KHR扩展,就可以让用户在windows平台上完成大部分的需求。

本文将要实现的Instance接口,只能看到我们所启用的Extension接口,而看不到程序没有启用的。这样可以避免调用未启用扩展的函数的错误。那么需求可以归纳如下:

  • 封装VkInstance;
  • 列出Instance需要的若干个扩展接口;
  • 合成一个Instance类型,包含提供的扩展接口和VkInstance,作为VkInstance的封装类。

除此之外,我们还希望所有的这些扩展接口可以方便地访问到VkInstance的实例。很自然地,可以想到这个合成的类型,应该是一个单继承的结构。

为了实现单继承的结构,我们需要如下的类模板:

template <typename Ext, typename Base>
class instance_extension;

接下来,实现core interface,这个是vulkan instance最基础的接口,它也是VkInstance的持有者。

// tag class
struct instance_core_t {};

// core interface
template <>
class instance_extension<instance_core_t, void>
{
public:
    instance_extension(global_t const& global, VKInstance instance)
        : instance_(instance)
    {
        global.get_proc(instance, "vkDestroyInstance", vkDestroyInstance);
        global.get_proc(instance, "vkEnumeratePhysicalDevices", vkEnumeratePhysicalDevices);
        global.get_proc(instance, "vkCreateDevice", vkCreateDevice);        
    }

    ~instance_extension()
    {
        if (nullptr != instance_)
            vkDestroyInstance(instance_, nullptr);
    }

    auto enumerate_physical_devices() const
    {
        uint32_t device_count;
        vkEnumeratePhysicalDevices(instance_, &device_count, nullptr);
        std::vector<VkPhysicalDevice> physical_devices{ device_count };
        vkEnumeratePhysicalDevices(instance_, &device_count, physical_devices.data());
        return physical_devices;
    }
    // ... omitted interfaces

protected:
    VkInstance get_instance() const noexcept
    {
        return instance_;
    }

protected:
    VkInstance                      instance_;
    PFN_vkDestroyInstance           vkDestroyInstance;
    PFN_vkEnumeratePhysicalDevices  vkEnumeratePhysicalDevices;
    PFN_vkCreateDevice              vkCreateDevice;
    // ... omitted instance level functions
};

对instance level functions也需要做一些简单的封装,而不是暴露C接口函数,例如enumerate_physical_devices. 封装的任务是提供一个更接近对象语义的接口。此外,core interface还要暴露一个保护类型的接口,get_intance, 以便扩展接口可以方便的访问VkInstance. 篇幅所限,本文只列出了部分函数和一个接口封装的实现。

接下来是扩展接口。扩展接口还要承接继承关系,最后的基类是core interface。以VK_KHR_surface扩展为例:

// tag class
namespace khr
{
    inline constexpr struct surface_ext_t
    {
        static char const* name() noexcept
        {
            return VK_KHR_SURFACE_EXTENSION_NAME;
        }
    } surface_ext;
}

// inheritance here
template <typename Base>
class instance_extension<khr::surface_ext_t, Base> : public Base
{
public:
    instance_extension(global_t const& g, VkInstance ins)
        : Base(g, ins)
    {
        assert(this->get_instance() == ins);
        g.get_proc(ins, "vkGetPhysicalDeviceSurfaceSupportKHR", vkGetPhysicalDeviceSurfaceSupportKHR);
        g.get_proc(ins, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR", vkGetPhysicalDeviceSurfaceCapabilitiesKHR);
        g.get_proc(ins, "vkGetPhysicalDeviceSurfacePresentModesKHR", vkGetPhysicalDeviceSurfacePresentModesKHR);
        g.get_proc(ins, "vkDestroySurfaceKHR", vkDestroySurfaceKHR);
        g.get_proc(ins, "vkGetPhysicalDeviceSurfaceFormatsKHR", vkGetPhysicalDeviceSurfaceFormatsKHR);
    }

    void destroy_surface(khr::surface_t s) const
    {
        destory_surface(s.get());
    }
    // ... omitted interfaces

protected:
    void destory_surface(VkSurfaceKHR surface) const
    {
        if (nullptr != surface)
            vkDestroySurfaceKHR(this->get_instance(), surface, nullptr);
    }

protected:
    PFN_vkGetPhysicalDeviceSurfaceSupportKHR        vkGetPhysicalDeviceSurfaceSupportKHR;
    PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR   vkGetPhysicalDeviceSurfaceCapabilitiesKHR ;
    PFN_vkGetPhysicalDeviceSurfacePresentModesKHR   vkGetPhysicalDeviceSurfacePresentModesKHR;
    PFN_vkDestroySurfaceKHR                         vkDestroySurfaceKHR;
    PFN_vkGetPhysicalDeviceSurfaceFormatsKHR        vkGetPhysicalDeviceSurfaceFormatsKHR;
};

单继承的结构无论是在内存的布局上还是接口的简洁性上都有很多优势,多继承的情况下要么使用type cast移动指针来尝试获取数据字段,要么对数据进行多份复制。以上面的代码作为扩展实现的蓝本,其他扩展的实现就是工作量的问题了。最后剩下一个将这些散列的扩展模板,与core interface链接的问题了。

链接扩展与core interface为一个聚合类,是一个类型计算的元函数,命名为generate_extensions_hierarchy, 先看看它的外观

template 
<
    template <typename, typename> class TE,
    typename ... Exts
>
struct generate_extensions_hierarchy;

第一个参数是一个模板的模板参数(template template parameter), 它是生成单继承链的中间环,也就是我们的instance_extension模板。Exts就是我们要启用的扩展。元函数是一个递归模板,它的终止条件就是Exts的variadic size为1的时候,萃取T为扩展应用到TE模板,第二个参数为void. 终止条件肯定为core interface.

template
<
    template <typename, typename> class TE,
    typename T
>
struct generate_extensions_hierarchy<TE, T>
{
    using type = TE<T, void>;
};

而它的递推式是萃取Exts…的第一个类型为扩展类型应用于TE模板。然后再继续利用元函数,递归推导余下的扩展类型,作为第二个模板参数,对应于instance_extension的Base参数,也就是基类。

template 
<
    template <typename, typename> class TE,
    typename T, typename ... Rests
>
struct generate_extensions_hierarchy<TE, T, Rests...>
{
    using type = TE<T, typename generate_extensions_hierarchy<TE, Rests...>::type>;
};

最后我们的instance类,可以这样实现:

template
<
    template <typename, typename> class TE,
    typename ... Exts
>
using generate_extensions_hierarchy_t = typename generate_extensions_hierarchy<TE, Exts...>::type;

template <typename T, typename Base>
using instance_extension_alias = instance_extension<T, Base>;

template <typename ... Exts>
class instance : public 
    generate_extensions_hierarchy_t<instance_extension_alias, Exts..., instance_core_t>
{
public:
    // implementation omitted
};

可以看到,core interface是扩展列表的最后一个,这里保证了它是终止条件。假设我们现在已经实现完毕了VK_KHR_surface和VK_KHR_win32_surface两个扩展,那么创建instance的简化代码如下:

class global_t
{
public:
    // ... omitted interface
    template <typename PFN>
    void get_proc(VkInstance instance, char const* name, PFN*& )

    template <typename ... Exts>
    auto create_instance(Exts ...)
    {
        return instance<Exts>{ *this, create_instance_handle()  };
    }

private:
    VkInstance create_instance_handle();

private:
    PFN_vkGetInstanceProcAddr   vkGetInstanceProcAddr;
    PFN_vkCreateInstance        vkCreateInstance;
};

auto& g = global_t::get();
auto instance = g.create_instance(khr::surface_win32_ext, khr::surface_ext);

下图显示了生成instance类的继承图表

graph TD
    A(instance) --> B("instance_extension< surface_win32_ext_t, instance_extension< khr::surface_ext_t, instance_extension< instance_core_t, void>>>")
    B --> C("instance_extension< khr::surface_ext, instance_extension< instance_core_t, void>>")
    C --> D("instance_extension< instance_core_t, void>")

5.小结

使用接口生成合成类,生成类型安全的接口,可以让我们避免调用为启用扩展的函数的错误。在Vulkan API的封装的实际应用中,不仅可以避免前面提到的错误,还可以在避免全局函数间接调用ICDs所带来的性能损失,同时也更符合vulkan中的概念. 本文中的详细代码可以参照这里, 目前只完成了Instance的封装,后续的实验性工作会继续进行。也欢迎来purecpp社区吐槽和交流。QQ群号:296561497.