Populating Structure from XML, the Automatic Way (Part 1 of 2)






3.40/5 (6 votes)
intros_ptree: A library that lets you populate your structure or class from XML file (or json or ini file) automatically, and vice versa
Introduction
Modern C++ is a game changer for C++ programming language. We can do some real cool stuff with it. Here, I present to you one such thing. Using boost property tree library and some modern C++ techniques, I will show you how we can populate a C++ structure or class object from xml, json or ini file automatically (almost) and vice versa.
This is a two part article series:
- In this part, I will show you how to use
intros_ptree
library. - And in the second part (link), we will go into the details of how the library works.
Even if you don’t care about xml files, you might still enjoy the article by watching the cool stuff that we can achieve using modern C++.
Attached with the article is intros_ptree
library, with examples on how to use it.
Prerequisites
Compiler: We will need a modern C++ compiler to use this library. I have tested this code against VS2015 update 1, GCC 5.2 and clang 3.6.
3rd party libraries: Boost. I have been using boost 1.60 while developing this library.
Before We Begin
This is a small C++ header only library. You can get the latest version from here.
You will need to add the following to compile the examples presented in this article:
#include <iostream>
#include <intros_ptree.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
using namespace std;
using namespace utils::intros_ptree;
using namespace boost::property_tree;
All the examples in this article are available in utils_examples.cpp file.
In the examples, I will be referring to xml file only, but the same applies for json and ini file too.
Using the Code
A Quick Introduction
Boost property tree library can open an xml, json or ini file and populate a property tree. Here is how we load a property tree
from an XML file:
ptree tree;
read_xml(filename, tree);
And here is how we write the tree
to an XML file:
write_xml(filename, tree);
And by using intros_ptree
library, we can populate a structure from the tree
and vice versa.
First, let’s create a structure that we want to be populated from the tree
:
struct test
{
int x;
string y;
;
Now, we need to define some internal data structure which will be used by the library (the almost part):
BEGIN_INTROS_TYPE(test)
ADD_INTROS_ITEM(x)
ADD_INTROS_ITEM(y)
END_INTROS_TYPE(test)
Now, to populate a test object from the tree, all we have to do is:
auto a = make_intros_object<test>(tree);
And, to populate a property tree from an object:
ptree tree = make_ptree(a);
And that’s it.
Let’s summarize the whole thing now with a concrete example.
Here is an example data:
string sample_xml = R"(
<book>
<id>102</id>
<name>i am a book</name>
<author>i am a writer</author>
</book>
)";
Here, we define the structure to hold the data:
struct book
{
int id;
string name;
string author;
};
We need to declare some internal data structure for the library:
BEGIN_INTROS_TYPE(book)
ADD_INTROS_ITEM(id)
ADD_INTROS_ITEM(name)
ADD_INTROS_ITEM(author)
END_INTROS_TYPE(book)
Now, to populate a book
object, all we have to do is:
ptree tree;
stringstream ss(sample_xml);
read_xml(ss, tree);
auto ob = make_intros_object<book>(tree);
And to write the data in XML file, all we have to do is:
ob.name = "new name"; // lets make some modification before writing
auto tree2 = make_ptree(ob);
xml_writer_settings<string> settings(' ', 4); // this is needed for xml printing
// to have proper whitespace
write_xml(cout, tree2, settings);
Description
intros_ptree
library gives us the following tools to work with it:
template<typename T>
boost::property_tree::ptree make_ptree(const T& in);
This function will accept any structure or class, for which intros
structure is defined. And it will return the ptree
for it.
template<typename T>
T make_intros_object(const boost::property_tree::ptree& tree);
This function will accept a ptree
, and will create a structure or class of type T
, as long as, intros
structure is defined for type T
.
So, we see, the functions let us get a ptree
from a type for which intros
structure is defined, and vice versa.
But to do this, we need to define the intros
structure. That’s where, the macros come in. Let’s see what they can do.
BEGIN_INTROS_TYPE(type)
With this macro, you start defining the intros
structure for your type.
END_INTROS_TYPE(type)
With this macro, you end your declaration for your type.
ADD_INTROS_ITEM(x)
With this macro, you add an item from your type to the intros
structure.
BEGIN_INTROS_TYPE_USER_NAME(type, name)
You will use this macro instead of BEGIN_INTROS_TYPE
when you need to set the name yourself, instead of using the default one. The default name is the name for your type.
ADD_INTROS_ITEM_USER_NAME(x, name)
Just like BEGIN_INTROS_TYPE_USER_NAME
, you will use ADD_INTROS_ITEM_USER_NAME
instead of ADD_INTROS_ITEM
when you need to use a different user name from the default one. The default user name is the name of your variable.
MAKE_USER_NAME(name, scope, is_attribute)
You will use this macro as the second parameter for ADD_INTROS_ITEM_USER_NAME
. You will need this, when you want to say, the item is in a different scope, or when you want to mark your item as an xml attribute. We will see some more of what they do in the coming examples.
Some More Examples
Once we have intros
support for our type, populating it from an xml file, or saving it into an xml file is trivial. Here is how we have to do it:
// here is how we populate a structure
ptree tree;
read_xml(xml_file_name, tree);
auto ob = make_intros_object<MyStruct>(tree);
// here is how we write the data to xml file
auto tree2 = make_ptree(ob);
xml_writer_settings<string> settings(' ', 4);
write_xml(xml_file_name, tree2, settings);
The interesting part is, how we add intros
support for our type. In the following examples, we will see how we have to add intros
support in different scenarios.
- Structure name and xml tag are different:
Let’s say, our xml data is like the following:
string sample_xml = R"( <root> <id>102</id> <name>i am a book</name> <author>i am a writer</author> </root> )";
And our structure is this:
struct book { int id; string name; string author; };
The xml tag says
root
, but the structure name isbook
. Then we need to defineintros
structure like below:BEGIN_INTROS_TYPE_USER_NAME(book, "root") ADD_INTROS_ITEM(id) ADD_INTROS_ITEM(name) ADD_INTROS_ITEM(author) END_INTROS_TYPE(book)
Lesson: Use
BEGIN_INTROS_TYPE_USER_NAME
when you need to use a name other than the type name. The second parameter is the new name you want to give your type. - Item name and xml tag are different:
This time, our xml structure is like below:
string sample_xml = R"( <book> <book_id>102</book_id> <name>i am a book</name> <author>i am a writer</author> </book> )";
And the structure is like below:
struct book { int id; string name; string author; };
Here, we need to map xml tag
book_id
withbook::id
. Here is, how we do this:BEGIN_INTROS_TYPE(book) ADD_INTROS_ITEM_USER_NAME(id, "book_id") ADD_INTROS_ITEM(name) ADD_INTROS_ITEM(author) END_INTROS_TYPE(book)
Lesson: Use
ADD_INTROS_ITEM_USER_NAME
when you want to use a name other than the variable name for your variable. - Multiple elements:
This time, our xml structure is like below:
string sample_xml = R"( <book> <book_id>102</book> <name>i am a book</name> <author>i am writer 1</author> <author>i am writer 2</author> <author>i am writer 3</author> </book> )";
And our structure is this:
struct book { int id; string name; vector<string> author; };
For this, we will define the
intros
structure like below:BEGIN_INTROS_TYPE(book) ADD_INTROS_ITEM_USER_NAME(id, "book_id") ADD_INTROS_ITEM(name) ADD_INTROS_ITEM(author) END_INTROS_TYPE(book)
Lesson: You can use containers just as easily as you would use other types.
- Adding scope to xml tags:
Let’s say, our xml data is like below this time:
string sample_xml = R"( <book> <book_id>102</book> <name>i am a book</name> <all_authors> <author>i am writer 1</author> <author>i am writer 2</author> <author>i am writer 3</author> </all_authors> </book> )";
Here,
author
element is added under a new tag,all_authors
. To support this, we need to define the intros structure like below:BEGIN_INTROS_TYPE(book) ADD_INTROS_ITEM_USER_NAME(id, "book_id") ADD_INTROS_ITEM(name) ADD_INTROS_ITEM_USER_NAME(author, MAKE_USER_NAME("author", "all_authors", false)) END_INTROS_TYPE(book)
Lesson: You can put an item to another scope (under a new xml element), with the help of
MAKE_USER_NAME
. Here, the first parameter is the name you want for your variable, and the second parameter is the scope name. The third parameter we will see in the next example. - Using xml attribute:
This time, our xml data is like this:
string sample_xml = R"( <book book_id="102"> <name>i am a book</name> <author>i am a writer</author> </book> )";
And structure is this:
struct book { int id; string name; string author; };
This time, we need to define the
intros
structure like below:BEGIN_INTROS_TYPE(book) ADD_INTROS_ITEM_USER_NAME(id, MAKE_USER_NAME("book_id", "", true)) ADD_INTROS_ITEM(name) ADD_INTROS_ITEM(author) END_INTROS_TYPE(book)
Lesson: The third parameter of
MAKE_USER_NAME
marks the item as an attribute. - Using type inside another type:
We can get
intros
support for a type that uses another type inside it, as long as, both of the types haveintros
support.This time, our xml data is like below:
string sample_xml = R"( <catalog> <name>I am the catalog</name> <book> <id>102</id> <name>i am a book</name> <author>i am writer 1</author> </book> <book> <id>103</id> <name>i am also book</name> <author>i am writer 2</author> </book> <book> <id>104</id> <name>i am another book</name> <author>i am writer 3</author> </book> </catalog> )";
And now, our structures are like this:
struct book { int id; string name; string author; }; struct catalog { string name; vector<book> books; };
Intros
definition forbook
is just like before:BEGIN_INTROS_TYPE(book) ADD_INTROS_ITEM(id) ADD_INTROS_ITEM(name) ADD_INTROS_ITEM(author) END_INTROS_TYPE(book)
And
intros
definition for catalog is also, what we would assume should be:BEGIN_INTROS_TYPE(catalog) ADD_INTROS_ITEM(name) ADD_INTROS_ITEM_USER_NAME(books, "book") END_INTROS_TYPE(catalog)
Lesson: We can use one type inside another type, for
intros_ptree
, as long as, both the types hasintros
support.
Additional Notes
- C style array (ex.
int a[10]
) is not supported asintros_item
. intros
definition must be in a global scope.- We cannot give a type two intros definitions. That is, for a type
T
, we can provide only oneBEGIN_INTROS_TYPE
/END_INTROS_TYPE
block.
Debug Tip
As you can see from the above examples, defining intros struct
is quite intuitive. And anytime, you feel you are not sure whether the intros struct
you defined is correct or not, you can print it and see the result. Let me give you an example of that with the above catalog structure:
catalog c;
c.books.resize(2);
ptree tree = make_ptree(c);
xml_writer_settings<string> settings(' ', 4); // this is needed for xml printing
// to have proper whitespace
write_xml(cout, tree, settings);
And this will print:
<?xml version="1.0" encoding="utf-8"?>
<catalog>
<name/>
<books>
<id>0</id>
<name/>
<author/>
</books>
<books>
<id>0</id>
<name/>
<author/>
</books>
</catalog>
This way, we can check, if we have the xml format to our liking.
And that’s the end of part 1. In the next part, I will show you, how intros_ptree
library achieves this using modern C++.