SVG属性类设计 (2)
需求
在SVG属性类设计(1)中,定义了基本SVG属性类,提供了泛型的实现方式。接下来我们需要让SVG属性能进一步适配组合型属性,例如"(a|b) (<number>)"的类型要求。(具体的例子:marker元素中的preserveAspectRatio
,其合法属性为"(none|xMinYMin|xMidYMin|xMaxYMin|xMinYMid|xMidYMid|xMaxYMid|xMinYMax|xMidYMax|xMaxYMax) (meet|slice)"
)。由于其性质和元组一致,称之为属性元组。
属性元组的特点是包含多个不同的类型,且依次展示,并通过某个连接符连接。
设计
由于属性元组实际上是一个容器,故应当没有之前基本属性中赋值、绑定的特性。取而代之的是获取某个位置的属性。并且作为属性,其基本功能中的构造、读、提交等都应当实现。
这是最终的设计。
-
底层容器
_tuple
是一个std::tuple容器,能够存储泛型对象;但这也为后续对象的设计带来了复杂性,因为std::tuple的泛型存储其实是在编译期通过元编程实现的,我们也必须使用元编程进行适配。 -
基本单元
基本单元就是容器中保存的Attribute[i]
,它是一些构造元组时就自动生成的对象,且顺序符合泛型列表中的顺序。当这些基本单元是SVG属性,元组才能生效。如果它们是SVG属性,则SVG属性与元组内部通过
_getter[i]
和_setter[i]
和_attr[i]
成员绑定。 -
At操作
针对元组的容器性质的操作。返回某个索引位置的对象的引用。 -
GetCommit操作
获取提交区的值。元组中的提交区实际上就是_attr[i]
,提交区值由其计算。这里有一个二次提交的过程:每次获取提交区的值时,需要调用
_commit
,把元组所维护的SVG属性Attribute[i]
的未提交的值提交到_attr[i]
中。 -
Commit操作
这是元组作为SVG元素的实际提交。通过自身维护的setter提交。
细节
-
参数包(Parameter Pack)展开
首先要解决的问题就是泛型容器操作的相关问题。其中一个重要问题就是如何遍历泛型容器。设计中_commit和构造都需要用到容器遍历,故优先级最高。
-
C++17
对于参数包
template <typename... Ts>
,在C++17中的展开方式十分简便,只需要通过Ts...
展开即可。并且:my_func(ts...);
和
my_func(t1), my_func(t2), ... my_func(tn)
等价。由此遍历tuple只需通过类似的原理进行。但是我们基于的是gcc-7.5.0,默认并不是支持C++17(可添加参数支持),因此需要寻找更广泛的实现方式。
-
C++11
这是参数包被引入的正式版本,其中已提供了一种展开参数包的方式:
template<std::size_t i, typename L> void _my_func() const { auto &u = std::get<i>(_tuple); _my_func_impl(u); } template<std::size_t i, typename F, typename S, typename ...R> void _my_func() const { auto &u = std::get<i>(_tuple); _my_func_impl(u); _my_func<i + 1, S, R...>(); } template<typename I> void _my_func_impl(I &u) const { //... }
这是利用元编程函数重载解析过程中的特性。对于第一个
_my_func
的重载,第一个模板参数表示递归深度,也就是模板泛型列表的索引值;第二个模板参数表示这是最后一个递归,仅支持一个泛型;所以递归展开到该函数就会结束。对于第二个_my_func
的重载,第一个模板参数和前面一致;第二个模板参数是当前泛型;最后一个模板参数是参数包;函数中最后一行进行递归;所以如果参数包中有多于一个模板参数时,会一直匹配到该函数,然后进行展开,直到仅剩一个模板参数。最后一个函数是具体实现,在上述例子中表示对元组_tuple
中取得的某个对象进行操作。对于合法类型的检验,则使用和之前类似的方法,通过成员模板重载实现。
-
-
模板对象的声明与定义
如果将模板对象的声明和定义分别写入一个.h头文件和一个.cpp实现文件中,则编译会一直报错:找不到声明。
实际上在前面实现模板成员函数时,也遇到过类似问题,解决方法是将.cpp文件直接#include到.h文件最后。但是模板对象仿佛并不能通过这样的方式解决,因为编译器找不到具体的声明。
解决方法:将.h和.cpp合并为.hpp,边声明边定义。
-
模板成员函数作为std::function
一开始是通过模板成员函数的方式表示
_getter[i]
和_setter[i]
,但后续无论如何都无法被std::function作为值接收。其间尝试过std::bind与对象绑定、static_cast解决模板函数的重载问题,但始终提示unresolved overloaded function type
。解决方法:使用lambda表达式,能够无视上述情况直接传递。
-
引用问题
在构造函数中,首先是发现
_getter[i]
和_setter[i]
无法正确传递,而后发现两对象的指针不一致,于是发现:auto u = std::get<i>(_tuple);
左值是拷贝。因此
u
和原对象不同。解决方法:改为
auto &u
。 -
只读问题
_commit
由get_commit
调用,因此也是只读的。但是随后在进行了(4)中的修改后发现递归中出现无法与重载匹配的情况。细察得知
std::get
在const函数中获取的是const T&
。解决方法:将
_commmit
中的auto &u
改回auto u
。这样虽是获取对象浅拷贝,但结果保证正确,因为SVG属性中的_commit仍然指向原对象。