1.1 缘起
stl中对线性表有充分的实现,无论是vector还是list都是典型的线性表,即便是set和map,尽管实现上采用了诸如红黑树之类的树形结构,但那仅仅是为了快速检索的需要,从语义上来说它们依旧是线性表,无法表达目录树这种树形结构。boost中的property_tree可以看做是对树形结构实现的补充,我们大可把它扩展应用到各种需要树形结构的地方。
当我们拥有了一个树性结构以后,那么也就同时拥有了树形结构中的每个节点的访问权,很自然的,我们希望每个节点都可以方便的检索(类似于输入一个文件路径就快速的定位到文件),同时我们也希望能在节点上存储点什么以支持各种实际的应用。这里可以和MFC的树形控件CTreeCtrl做一下类比,MFC中的CTreeCtrl不支持类似于路径的检索,好在CTreeCtrl的节点中有一个字符串类型的名称,使得我们可以通过字符串和不断的GetChildItem,GetNextItem自己去检索定位;同时它的每个节点都可以存储一个int数据,并通过方法SetItemData和GetItemData进行读取,然后再通过这个int数据再关联到那些比int更复杂的数据结构。这种通过int数据二次关联的实现是能够工作的,只是有些繁琐。而在boost的泛型世界里,再如此硬编码一个类型肯定会被人笑话的。标准的做法是为树形结构的每个节点关联一个模板Key类型的索引数据和一个Data类型的存储数据,至于Key和Data类型是什么,爱是谁是谁。这个被关联的Data类型的数据就是节点的属性(property)。说了这么多,我们终于可以解开property_tree的真面目了:
template<class Key, class Data, class KeyCompare> class basic_ptree { typedef basic_ptree<Key, Data, KeyCompare> self_type; public: // Basic types typedef Key key_type; typedef Data data_type; typedef KeyCompare key_compare; // Container view types typedef std::pair<const Key, self_type> value_type; typedef std::size_t size_type; private: // Hold the data of this node data_type m_data; // Hold the children - this is a void* because we can't complete the // container type within the class. void* m_children; };
其中的m_children又到底是什么类型呢?需要看下面的实现代码:
template<class K, class D, class C> inline basic_ptree<K, D, C>::basic_ptree() : m_children(new typename subs::base_container) { } template <class K, class D, class C> struct basic_ptree<K, D, C>::subs { struct by_name {}; // The actual child container. typedef multi_index_container<value_type, multi_index::indexed_by< multi_index::sequenced<>, multi_index::ordered_non_unique<multi_index::tag<by_name>, multi_index::member<value_type, const key_type, &value_type::first>, key_compare > > > base_container; };
multi_index也是boost中的一个组件,作用是为一个数据结构提供多种方式的索引,我们先不纠结于multi_index的细节,可以把它简化为list<value_type>,而value_type又是一个std::pair<const Key, self_type>。好了,整个树形的数据结构就串起来了,每个节点都保存一个data_type m_data,即一个Data类型的属性值,同时还保存一个list<value_type>,即以Key标识的所有子节点列表。
1.2 遍历
了解了property_tree的数据结构,树的遍历就很简单了。
首先,我们可以通过data方法得到节点上的属性值:
template<class K, class D, class C> inline typename basic_ptree<K, D, C>::data_type & basic_ptree<K, D, C>::data() { return m_data; }
其次,我们可以构造所有子节点的迭代器:
template <class K, class D, class C> class basic_ptree<K, D, C>::iterator : public boost::iterator_adaptor< iterator, typename subs::base_container::iterator, value_type> { friend class boost::iterator_core_access; typedef boost::iterator_adaptor< iterator, typename subs::base_container::iterator, value_type> baset; public: typedef typename baset::reference reference; iterator() {} explicit iterator(typename iterator::base_type b) : iterator::iterator_adaptor_(b) {} reference dereference() const { // multi_index doesn't allow modification of its values, because // indexes could sort by anything, and modification screws that up. // However, we only sort by the key, and it's protected against // modification in the value_type, so this const_cast is safe. return const_cast<reference>(*this->base_reference()); } };
基本上直接封装内部数据结构的迭代器就可以了。剩下的就是stl的迭代器的标准用法了。
现在,我们可以直接写出property_tree的遍历函数了,这里采用递归处理,以树形结构的打印为例,同时为了打印的方便,我们采用ptree:
//typedef basic_ptree<std::string, std::string> ptree; void print(ostream& os, const ptree& pt, int tab) { tab += 2; for (ptree::const_iterator iter = pt.begin(); iter != pt.end(); ++iter) { os << string(tab, ' '); os << "{" << iter->first << "}" << "[" << iter->second.data() << "]\n"; print(os, iter->second, tab); } }
这里处理了tab的缩进,为什么一上来就+2?因为最外层的根节点其实是文档本身,而我们一般会跳过它。
1.3 Xml文件的读取
从property_tree的实现代码可以看到,其内部的xml解析采用了rapidxml,明晃晃的rapidxml.hpp是那么的显眼。这个号称是执行最快的xml解析的实现,既然要快,就不可能像xerces那样的庞大臃肿,步履蹒跚,当然也不可能象后者一样的面面俱到。(私下以为,如果property_tree敢用xerces,它也不可能被boost通过,呵呵)。
实现细节rapidxml被很好地封装起来,以保证未来实现部分的可替换性。对外直接给了read_xml和write_xml两个方法处理xml文件的读写,这里先看看read_xml方法:
template<class Ptree> void read_xml(const std::string &filename, Ptree &pt, int flags = 0, const std::locale &loc = std::locale()) { BOOST_ASSERT(validate_flags(flags)); std::basic_ifstream<typename Ptree::key_type::value_type> stream(filename.c_str()); if (!stream) BOOST_PROPERTY_TREE_THROW(xml_parser_error( "cannot open file", filename, 0)); stream.imbue(loc); read_xml_internal(stream, pt, flags, filename); }
好了,是时候写出property_tree读取xml的示例代码了:
void read_xml_demo() { try { ptree doc; read_xml("config/config.xml", doc, xml_parser::trim_whitespace); print(cout, doc, -2); } catch (xml_parser_error& e) { cout << e.what() << endl; } catch (std::exception& e) { cout << e.what() << endl; } }
代码非常简单,不解释了。这里示例的config.xml是下面的内容:
<?xml version="1.0" encoding="GB2312"?> <config> <main title="windows" icon="main.ico"> <!-- Main Fisrt Comment --> <!-- Main Second Comment --> </main> <paths name="init"> <!-- Paths Comment --> <path level="1">a.ini</path> <path level="2">b.ini</path> <path level="3">c.ini</path> </paths> <links key="<>"> <!-- Links Comment --> <link level="1">www.sina.com</link> <link level="2">www.sohu.com</link> <link level="3">www.163.com</link> </links> </config>
但是你能想到会打印出什么东西吗?这里其实有一个非常严重的问题:数据结构的不对等。
我们知道,xml本身结构是比较复杂的,节点的类型有:
enum node_type { node_document, //!< A document node. Name and value are empty. node_element, //!< An element node. Name contains element name. Value contains text of first data node. node_data, //!< A data node. Name is empty. Value contains data text. node_cdata, //!< A CDATA node. Name is empty. Value contains data text. node_comment, //!< A comment node. Name is empty. Value contains comment text. node_declaration, //!< A declaration node. Name and value are empty. Declaration parameters (version, encoding and standalone) are in node attributes. node_doctype, //!< A DOCTYPE node. Name is empty. Value contains DOCTYPE text. node_pi //!< A PI node. Name contains target. Value contains instructions. };
(摘自rapidxml.hpp。)
每个元素下还有一组属性和一个可选的text。
但是我们知道property_tree的数据结构是比较简单的,并不能支持xml这样的复杂结构。这个问题我估计也是让property_tree的作者很纠结的问题,被逼无奈之下,他采用了一个能工作但很难看的解决方案:字符串的私有协议。简单的说协议有两条:
- 字符串”<xmlattr>” 表示该节点是xml属性
- 字符串”<xmlcomment>” 表示该节点是xml注释
这样,当我们看到下面的输出结果就不会感到惊讶了:
{config}[] {main}[] {<xmlattr>}[] {title}[windows] {icon}[main.ico] {<xmlcomment>}[ Main Fisrt Comment ] {<xmlcomment>}[ Main Second Comment ] {paths}[] {<xmlattr>}[] {name}[init] {<xmlcomment>}[ Paths Comment ] {path}[a.ini] {<xmlattr>}[] {level}[1] {path}[b.ini] {<xmlattr>}[] {level}[2] {path}[c.ini] {<xmlattr>}[] {level}[3] {links}[] {<xmlattr>}[] {key}[<>] {<xmlcomment>}[ Links Comment ] {link}[www.sina.com] {<xmlattr>}[] {level}[1] {link}[www.sohu.com] {<xmlattr>}[] {level}[2] {link}[www.163.com] {<xmlattr>}[] {level}[3]
1.4 其它文件格式的读取
我在文章的开头就说过了property_tree是为树形结构而生的,而不仅仅只为xml而生的,因此property_tree还支持json、info、ini几种文件格式。了解了xml文件的读取后,这几种文件格式的读取也就比较简单了。
读取这三种文件的示例代码如下:
void read_json_demo() { try { ptree doc; read_json("config/config.json", doc); print(cout, doc, -2); } catch (json_parser_error& e) { cout << e.what() << endl; } catch (std::exception& e) { cout << e.what() << endl; } }; void read_ini_demo() { try { ptree doc; read_ini("config/config.ini", doc); print(cout, doc, -2); } catch (json_parser_error& e) { cout << e.what() << endl; } catch (std::exception& e) { cout << e.what() << endl; } }; void read_info_demo() { try { ptree doc; read_info("config/config.info", doc); print(cout, doc, -2); } catch (json_parser_error& e) { cout << e.what() << endl; } catch (std::exception& e) { cout << e.what() << endl; } };
如你所见,几乎是一个模子倒出来的,除了调用的读取方法略有区别。
这里统一给出以上代码所需要引用的头文件和命名空间:
#include <iostream> #include <boost/property_tree/ptree.hpp> #include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/json_parser.hpp> #include <boost/property_tree/ini_parser.hpp> #include <boost/property_tree/info_parser.hpp> using namespace boost; using namespace std; using namespace boost::property_tree;
为了节省点读者的时间,我就再费点事,把三个格式的示例文件都贴出来吧。
示例文件config.json如下:
{ "config" : { "main" : { "title" : "windows", "icon" : "main.ico", "comment" : "Main Fisrt Comment", "comment" : "Main Second Comment" }, "paths" : { "name" : "init", "comment" : "Paths Comment", "path" : { "level" : "1", "text" : "a.ini" }, "path" : { "level" : "2", "text" : "b.ini" }, "path" : { "level" : "3", "text" : "c.ini" } }, "links" : { "key" : "<>", "comment" : "Links Comment", "link" : { "level" : "1", "text" : "www.sina.com" }, "link" : { "level" : "2", "text" : "www.sohu.com" }, "link" : { "level" : "3", "text" : "www.163.com" } } } }
示例文件config.info如下:
config { main { title windows icon main.ico comment Main Fisrt Comment comment Main Second Comment } paths { name init comment Paths Comment path { level 1 text a.ini }, path { level 2 text b.ini }, path { level 3 text c.ini } } links { key <> comment Links Comment link { level 1 text www.sina.com } link { level 2 text www.sohu.com } link { level 3 text www.163.com } } }
示例文件config.ini如下:
[config.main] title="windows" icon="main.ico" [config.paths] name="init" [config.paths.path1] level="1" text=a.ini [config.paths.path2] level="2" text=b.ini [config.paths.path3] level="3" text=c.ini [config.links] key="<>" [config.links.link1] level="1" text=www.sina.com [config.links.link2] level="2" text=www.sohu.com [config.links.link3] level="3" text=www.163.com
1.5 路径和访问
了解了xml文件的读取,下面就要关心树形结构的访问了。在文章的开头我说过,property_tree支持类似于路径的快速访问机制,这是通过path_type实现的:
template <typename Key> struct path_of; template <typename Ch, typename Traits, typename Alloc> struct path_of< std::basic_string<Ch, Traits, Alloc> > { typedef std::basic_string<Ch, Traits, Alloc> _string; typedef string_path< _string, id_translator<_string> > type; }; typedef typename path_of<Key>::type path_type;
我们看到,path_type特化后其实就是一个字符串,与我们熟知的目录路径是一致的。我们首先看一下如何根据path定位?这可以调试get_child函数的调用堆栈:
template<class K, class D, class C> basic_ptree<K, D, C> & basic_ptree<K, D, C>::get_child(const path_type &path) { path_type p(path); self_type *n = walk_path(p); if (!n) { BOOST_PROPERTY_TREE_THROW(ptree_bad_path("No such node", path)); } return *n; } template<class K, class D, class C> basic_ptree<K, D, C> * basic_ptree<K, D, C>::walk_path(path_type &p) const { if(p.empty()) { // I'm the child we're looking for. return const_cast<basic_ptree*>(this); } // Recurse down the tree to find the path. key_type fragment = p.reduce(); const_assoc_iterator el = find(fragment); if(el == not_found()) { // No such child. return 0; } // Not done yet, recurse. return el->second.walk_path(p); } template<class K, class D, class C> inline typename basic_ptree<K, D, C>::const_assoc_iterator basic_ptree<K, D, C>::find(const key_type &key) const { return const_assoc_iterator(subs::assoc(this).find(key)); } static const by_name_index& assoc(const self_type *s) { return ch(s).BOOST_NESTED_TEMPLATE get<by_name>(); } static const base_container& ch(const self_type *s) { return *static_cast<const base_container*>(s->m_children); } template<typename Tag> const typename index<Tag>::type& get()const BOOST_NOEXCEPT { return *this; }
我们看到,第一个函数get_child会调用walk_path进行实际的检索,而第二个函数walk_path会将这个工作转给find,第三个函数find则直接找节点内部的那个子节点列表数据结构了。
了解了get_child函数,我们再看add函数:
template<class K, class D, class C> template<class Type> inline basic_ptree<K, D, C> & basic_ptree<K, D, C>::add( const path_type &path, const Type &value) { return add(path, value, typename translator_between<data_type, Type>::type()); } template<class K, class D, class C> template<class Type, typename Translator> inline basic_ptree<K, D, C> & basic_ptree<K, D, C>::add( const path_type &path, const Type &value, Translator tr) { self_type &child = add_child(path, self_type()); child.put_value(value, tr); return child; } template<class K, class D, class C> basic_ptree<K, D, C> & basic_ptree<K, D, C>::add_child(const path_type &path, const self_type &value) { path_type p(path); self_type &parent = force_path(p); // Got the parent. key_type fragment = p.reduce(); return parent.push_back(value_type(fragment, value))->second; }
第一个add函数增加了translator参数,第二个add函数先用add_child增加子节点,再用put_value写属性值,第三个add_child函数,代码也比较简单,在路径定位后,在父节点的数据结构中增加value_type即可。
注意到上面写节点属性值用到了put_value方法,为什么不直接用data方法?我们知道,property_tree中节点上的属性数据可以用方法data直接访问,但我们读写的可不一定是一定是Data类型的数据,因此这里就需要put和get的泛型封装了。
首先看看get,我们根据get函数执行的调用堆栈,依此找到了以下函数:
template<class K, class D, class C> template<class Type> inline Type basic_ptree<K, D, C>::get_value() const { return get_value<Type>( typename translator_between<data_type, Type>::type()); } template<class K, class D, class C> template<class Type, class Translator> typename boost::enable_if<detail::is_translator<Translator>, Type>::type basic_ptree<K, D, C>::get_value(Translator tr) const { if(boost::optional<Type> o = get_value_optional<Type>(tr)) { return *o; } BOOST_PROPERTY_TREE_THROW(ptree_bad_data( std::string("conversion of data to type \"") + typeid(Type).name() + "\" failed", data())); } template<class K, class D, class C> template<class Type, class Translator> inline optional<Type> basic_ptree<K, D, C>::get_value_optional( Translator tr) const { return tr.get_value(data()); } boost::optional<E> get_value(const internal_type &v) { std::basic_istringstream<Ch, Traits, Alloc> iss(v); iss.imbue(m_loc); E e; customized::extract(iss, e); if(iss.fail() || iss.bad() || iss.get() != Traits::eof()) { return boost::optional<E>(); } return e; }
第一个函数get_value为函数调用增加了translator参数。第二个函数get_value通过boost::optional增加了节点是否存在的判断。第三个函数get_value_optional增加了data()参数,并让Translator去解决无问题,第四个函数get_value是在Translator内部对data()进行最后的转换。
再来看put函数:
template<class K, class D, class C> template<class Type> inline basic_ptree<K, D, C> & basic_ptree<K, D, C>::put( const path_type &path, const Type &value) { return put(path, value, typename translator_between<data_type, Type>::type()); } template<class K, class D, class C> template<class Type, typename Translator> basic_ptree<K, D, C> & basic_ptree<K, D, C>::put( const path_type &path, const Type &value, Translator tr) { if(optional<self_type &> child = get_child_optional(path)) { child.get().put_value(value, tr); return *child; } else { self_type &child2 = put_child(path, self_type()); child2.put_value(value, tr); return child2; } } template<class K, class D, class C> template<class Type, class Translator> void basic_ptree<K, D, C>::put_value(const Type &value, Translator tr) { if(optional<data_type> o = tr.put_value(value)) { data() = *o; } else { BOOST_PROPERTY_TREE_THROW(ptree_bad_data( std::string("conversion of type \"") + typeid(Type).name() + "\" to data failed", boost::any())); } } boost::optional<internal_type> put_value(const E &v) { std::basic_ostringstream<Ch, Traits, Alloc> oss; oss.imbue(m_loc); customized::insert(oss, v); if(oss) { return oss.str(); } return boost::optional<internal_type>(); }
第一个put函数增加了translator参数,第二个put函数通过get_child_optional增加了节点是否存在的判断,第三个put_value函数让Translator去解决问题,第四个put_value是在Translator内部对数据进行最后的转换。
本节的测试代码如下:
void visit_xml_demo(ptree& doc) { ptree root = doc.get_child("config.main"); root.add("<xmlattr>.ver", "1"); string title = root.get<string>("<xmlattr>.title"); assert(title == "windows"); int ver = root.get<int>("<xmlattr>.ver"); assert(ver == 1); root.put("<xmlattr>.ver", 2); }
代码比较简单,多跟踪几遍就一清二楚了。
1.6 xml文件的写入
了解了xml文件的读取,写入还很困难吗?确实,不过是read_xml和write_xml的区别而已:
template<class Ptree> void write_xml(const std::string &filename, const Ptree &pt, const std::locale &loc = std::locale(), const xml_writer_settings< typename Ptree::key_type > & settings = xml_writer_settings<typename Ptree::key_type>()) { std::basic_ofstream<typename Ptree::key_type::value_type> stream(filename.c_str()); if (!stream) BOOST_PROPERTY_TREE_THROW(xml_parser_error( "cannot open file", filename, 0)); stream.imbue(loc); write_xml_internal(stream, pt, filename, settings); }
如果仅仅是xml的read_xml然后write_xml确实没什么麻烦的,因为read_xml的时候其实已经建立的整个树形结构,然后再保存就可以了。这里真正需要注意的是如果没有xml文件呢?该如何凭空构造一个树形结构?通过对路径和访问一节的学习,这其实也不难,不过是反复调用add或add_child而已。这里还需要注意的是property_tree采用的依然是stl的值语义,因此只有在子节点都设置好了之后再加入父节点才能保证数据不丢失。
void write_xml_demo() { ptree root; ptree file_node; file_node.add("<xmlattr>.title", "windows"); file_node.add("<xmlattr>.size", "10Mb"); root.add_child("file", file_node); root.add("<xmlcomment>", "File Fisrt Comment"); root.add("<xmlcomment>", "File Second Comment"); { ptree paths_node; paths_node.add("<xmlattr>.attr", "directory"); paths_node.add("<xmlcomment>", "Paths Comment"); ptree path_node; path_node.add("<xmlattr>.title", "北京"); path_node.put_value("abc"); paths_node.add_child("path", path_node); path_node = ptree(); path_node.add("<xmlattr>.title", "上海"); path_node.put_value("efg"); paths_node.add_child("path", path_node); path_node = ptree(); path_node.add("<xmlattr>.title", "广州"); path_node.put_value("hij"); paths_node.add_child("path", path_node); root.add_child("paths", paths_node); } { ptree paths_node; ptree path_node; path_node.add("<xmlattr>.title", "111"); path_node.put_value("klm"); paths_node.add_child("path", path_node); path_node = ptree(); path_node.add("<xmlattr>.title", "222"); path_node.put_value("nop"); paths_node.add_child("path", path_node); path_node = ptree(); path_node.add("<xmlattr>.title", "333"); path_node.put_value("qrs"); paths_node.add_child("path", path_node); root.add_child("paths", paths_node); } ptree doc; doc.add_child("config", root); try { xml_writer_settings<string> settings('\t', 1, "GB2312"); write_xml("config/config.xml", doc, std::locale(), settings); } catch (std::exception& e) { cout << e.what() << endl; } }
代码比较简单,不解释了。其它几种格式的文件也类似。唯一需要注意的是因为ini文件无法支持超过2层的树形结构,注意一下树形结构的层数就是了。
1.7 UNICODE和UTF-8
以上的示例代码通通都是string和GB2312的,那么property_tree是否支持UNICODE和UTF-8呢?我第一次遇到这个问题是在解析SVG文件的时候,几乎收集上来的所有SVG文件都是UTF-8的,而且我们的开发环境又必须支持UNICODE。因为read_xml和write_xml两个方法中都有locale的参数,因此这个需求是很容易实现,唯一不爽的是boost的UTF-8转换需要引入utf8_codecvt_facet.hpp,而这又是必须编译成库才能使用组件。我知道这有些偏执了,尽管大多数boost组件不需要编译成库,但是我们永远也不能保证我们就一定不会用那些需要编译成库的组件。那么好吧,编译就编译吧。
#include <iostream> #include <boost/lexical_cast.hpp> #include <boost/property_tree/ptree.hpp> #include <boost/property_tree/xml_parser.hpp> #ifndef _UNICODE #define tptree boost::property_tree::ptree #else #define tptree boost::property_tree::wptree #endif #define BOOST_ALL_DYN_LINK #include <boost/program_options/detail/convert.hpp> #include <boost/program_options/detail/utf8_codecvt_facet.hpp> using namespace std; using namespace boost; using namespace ws; using namespace boost::property_tree; void load_svg(tptree& doc, const string& path) { try { // 其中new的facet会被locale自动delete locale current_locale(locale(""), new program_options::detail::utf8_codecvt_facet()); read_xml(path, doc, xml_parser::trim_whitespace, current_locale); } catch (std::exception& e) { cout << e.what() << endl; } } void save_svg(tptree& doc, const string& path) { try { // 其中new的facet会被locale自动delete locale current_locale(locale(""), new program_options::detail::utf8_codecvt_facet()); xml_parser::xml_writer_settings<wstring> settings(_T('\t'), 1, _T("utf-8")); write_xml(path, doc, current_locale, settings); } catch (std::exception& e) { cout << e.what() << endl; } }
代码简单得几乎不需要解释,正好做一个轻松的结尾吧。如果以后再遇到property_tree中值得写成文字的内容,我会另开一篇文章。