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"; auto tree2 = make_ptree(ob);
xml_writer_settings<string> settings(' ', 4); 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:
ptree tree;
read_xml(xml_file_name, tree);
auto ob = make_intros_object<MyStruct>(tree);
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 is book
. Then we need to define intros
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
with book::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 have intros
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 for book
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 has intros
support.
Additional Notes
- C style array (ex.
int a[10]
) is not supported as intros_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 one BEGIN_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); write_xml(cout, tree, settings);
And this will print:
="1.0"="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++.