C++ has no XML parser in its standard library, so reading XML means picking a third party library. For most projects the answer is pugixml: it is a single pair of source files, it is fast, it has real XPath support, and the API is pleasant. TinyXML2 is the alternative when you want the absolute smallest dependency, RapidXML when you need raw speed on a document you can modify in place, and libxml2 when you need validation, namespaces and XSLT.
sudo apt install libpugixml-dev or vcpkg install pugixml), include <pugixml.hpp>, call doc.load_file("data.xml"), check the returned parse result, then walk with doc.child("root").children("item"). Build with g++ -std=c++20 -Wall -Wextra main.cpp -o main -lpugixml.Both pugixml and TinyXML2 examples below were compiled and run with GCC 13.3 against the distribution packages on Ubuntu 24.04: pugixml 1.14 and TinyXML2 10.0.0. The current upstream releases are pugixml 1.16 and TinyXML2 11.0.0, and nothing shown here changed between those versions. Every output block is the real output from those runs.
The sample document
All the parsers below read this file, saved as books.xml next to the executable.
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="b101" stock="4">
<title>The Pragmatic Programmer</title>
<author>Andrew Hunt</author>
<price currency="USD">39.95</price>
</book>
<book id="b102" stock="0">
<title>Effective Modern C++</title>
<author>Scott Meyers</author>
<price currency="USD">44.50</price>
</book>
<book id="b103" stock="12">
<title>Designing Data Intensive Applications</title>
<author>Martin Kleppmann</author>
<price currency="USD">54.00</price>
</book>
</catalog>Installing and building
Get the library in place before you write any code. On Ubuntu the packaged versions are a release or two behind upstream, which is fine for the APIs used here.
# Debian and Ubuntu
sudo apt install libpugixml-dev libtinyxml2-dev
# macOS
brew install pugixml tinyxml2
# Cross platform, any of the three
vcpkg install pugixml tinyxml2
conan install --requires=pugixml/1.16
# Confirm what you actually got
pkg-config --modversion pugixml tinyxml2Both ship a CMake config package, so integration is three lines. I built and ran the first example below through exactly this file to confirm it works.
cmake_minimum_required(VERSION 3.16)
project(xmldemo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(pugixml REQUIRED)
add_executable(xmldemo main.cpp)
target_link_libraries(xmldemo PRIVATE pugixml::pugixml)For TinyXML2 the target is tinyxml2::tinyxml2 and the package name is tinyxml2. RapidXML has no build step at all: drop the headers in your tree and include them. libxml2 exposes a pkg-config file named libxml-2.0, which CMake finds through PkgConfig.
Reading XML with pugixml
The whole document is loaded into a DOM tree in one call. Notice that the parse result converts to bool, and that it carries a byte offset so you can point at the problem in a large file.
#include <iostream>
#include <pugixml.hpp>
int main() {
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("books.xml");
if (!result) {
std::cerr << "parse failed: " << result.description()
<< " at offset " << result.offset << '\n';
return 1;
}
for (pugi::xml_node book : doc.child("catalog").children("book")) {
std::cout << book.attribute("id").value() << " | "
<< book.child("title").child_value() << " | "
<< book.child("author").child_value() << " | "
<< book.child("price").text().as_double() << " "
<< book.child("price").attribute("currency").value()
<< " | stock " << book.attribute("stock").as_int()
<< '\n';
}
return 0;
}g++ -std=c++20 -Wall -Wextra pugi.cpp -o pugi -lpugixml
./pugi
# b101 | The Pragmatic Programmer | Andrew Hunt | 39.95 USD | stock 4
# b102 | Effective Modern C++ | Scott Meyers | 44.5 USD | stock 0
# b103 | Designing Data Intensive Applications | Martin Kleppmann | 54 USD | stock 12book.child("isbn").child_value() on a book with no isbn gives you an empty string. That makes optional fields easy, but it also means a typo in a tag name fails silently. Assert on the nodes you require.XPath queries with pugixml
This is the feature that puts pugixml ahead of TinyXML2 and RapidXML for anything beyond a flat list. Instead of nesting loops and conditionals, you describe the nodes you want.
#include <iostream>
#include <pugixml.hpp>
int main() {
pugi::xml_document doc;
if (!doc.load_file("books.xml")) {
return 1;
}
pugi::xpath_node_set in_stock = doc.select_nodes("/catalog/book[@stock > 0]");
std::cout << "in stock: " << in_stock.size() << '\n';
for (const pugi::xpath_node& hit : in_stock) {
std::cout << " " << hit.node().child("title").child_value() << '\n';
}
pugi::xpath_node cheapest = doc.select_node("/catalog/book[not(../book/price < price)]");
if (cheapest) {
std::cout << "cheapest: " << cheapest.node().child("title").child_value() << '\n';
}
pugi::xml_node meyers = doc.select_node("//book[author='Scott Meyers']").node();
std::cout << "Meyers id: " << meyers.attribute("id").value() << '\n';
return 0;
}# in stock: 2
# The Pragmatic Programmer
# Designing Data Intensive Applications
# cheapest: The Pragmatic Programmer
# Meyers id: b102Three queries, each of which would be a dozen lines of manual traversal. A malformed XPath expression throws pugi::xpath_exception, so wrap dynamic query strings in a try block. Static expressions like these are worth precompiling into a pugi::xpath_query object if you run them in a loop.
Reading XML with TinyXML2
TinyXML2 is two files, one header and one source, with no dependencies and no exceptions. The API is pointer based and returns error codes, so it feels closer to C than pugixml does.
#include <iostream>
#include <tinyxml2.h>
using namespace tinyxml2;
int main() {
XMLDocument doc;
XMLError err = doc.LoadFile("books.xml");
if (err != XML_SUCCESS) {
std::cerr << "load failed: " << XMLDocument::ErrorIDToName(err) << '\n';
return 1;
}
XMLElement* catalog = doc.FirstChildElement("catalog");
if (catalog == nullptr) {
std::cerr << "no <catalog> root\n";
return 1;
}
for (XMLElement* book = catalog->FirstChildElement("book");
book != nullptr;
book = book->NextSiblingElement("book")) {
const char* id = book->Attribute("id");
int stock = 0;
book->QueryIntAttribute("stock", &stock);
XMLElement* title = book->FirstChildElement("title");
XMLElement* price = book->FirstChildElement("price");
double amount = 0.0;
if (price != nullptr) {
price->QueryDoubleText(&amount);
}
std::cout << (id ? id : "?") << " | "
<< (title ? title->GetText() : "?") << " | "
<< amount << " | stock " << stock << '\n';
}
return 0;
}g++ -std=c++20 -Wall -Wextra tiny.cpp -o tiny -ltinyxml2
./tiny
# b101 | The Pragmatic Programmer | 39.95 | stock 4
# b102 | Effective Modern C++ | 44.5 | stock 0
# b103 | Designing Data Intensive Applications | 54 | stock 12GetText() returns null for an element with no text child as well as for one that does not exist. Dereferencing without a check is the single most common way to crash a TinyXML2 program. Check every pointer, as the loop above does.Error handling
Real XML from the network or from users is malformed regularly. Neither library throws by default, so you have to look at the result.
#include <iostream>
#include <string>
#include <pugixml.hpp>
int main() {
const char* broken = "<catalog><book><title>Unclosed</book></catalog>";
pugi::xml_document doc;
pugi::xml_parse_result r = doc.load_string(broken);
if (!r) {
std::cerr << "status " << static_cast<int>(r.status) << '\n';
std::cerr << "reason " << r.description() << '\n';
std::cerr << "offset " << r.offset << '\n';
std::cerr << "near " << std::string(broken).substr(
static_cast<size_t>(r.offset), 20) << '\n';
return 1;
}
std::cout << "parsed cleanly\n";
return 0;
}# status 14
# reason Start-end tags mismatch
# offset 32
# near book></catalog>The offset pointing straight at the mismatched closing tag is what makes this usable in production. Log the description and the offset together and a support ticket about a bad feed becomes a two minute investigation.
Comparing the four libraries
| Library | License | API style | XPath | Best for |
|---|---|---|---|---|
| pugixml | MIT | Value handles, range based for | Yes, XPath 1.0 | The default choice |
| TinyXML2 | zlib | Raw pointers, error codes | No | Smallest possible dependency |
| RapidXML | Boost or MIT | Header only, in place parse | No | Maximum throughput |
| libxml2 | MIT | C API, manual cleanup | Yes, plus XSLT | Schema validation, namespaces |
On raw parse throughput, RapidXML is generally the fastest of the four because it parses destructively in place and hands you pointers into your own buffer, doing almost no allocation. pugixml is close behind and does not require you to keep the source buffer alive. Unless you have profiled and found XML parsing on your hot path, the difference will not be what limits your program.
The licensing column is the one that decides things in commercial work. All four are permissive, so none of them will force you to publish source. libxml2 is the heaviest dependency by a wide margin, and the only one whose C API requires you to free nodes and documents by hand. Wrap its handles in a std::unique_ptr with a custom deleter the moment you adopt it.
When to reach for libxml2 instead
The three lightweight parsers all read well formed XML and stop there. libxml2 is the one that answers a different question: is this document valid according to a schema, and what do these namespace prefixes actually mean?
| Need | pugixml | TinyXML2 | libxml2 |
|---|---|---|---|
| Well formedness check | Yes | Yes | Yes |
| XSD or DTD validation | No | No | Yes |
| Namespace resolution | Prefix is part of the name | Prefix is part of the name | Full support |
| Streaming a file too large for RAM | Limited | No | Yes, reader API |
| XSLT transforms | No | No | Yes, with libxslt |
Note that none of these examples were run against libxml2 on the machine used for this guide, because its development package was not installed there. The rows above describe documented capability, not measured behavior. If you adopt it, budget time for the C ownership model: every document, node set and XPath context you create has a matching free function you must call.
Troubleshooting
fatal error: pugixml.hpp: No such file or directory. The development headers are not installed. On Debian or Ubuntu, sudo apt install libpugixml-dev. With vcpkg, vcpkg install pugixml and then let the toolchain file handle the include path.
undefined reference to pugi::xml_document::load_file. You compiled but did not link. Add -lpugixml at the end of the command line, after your source files. Order matters with GNU ld.
load_file returns a file not found status. The path is relative to the working directory, not to the executable. Print the result description, which distinguishes a missing file from malformed content, and pass an absolute path while you debug.
Element lookups return nothing on a document with namespaces. pugixml and TinyXML2 do not resolve namespace prefixes. The node name is literally ns:book, prefix included. Match on the full prefixed name, or move to libxml2 if you need real namespace handling.
Non ASCII text comes out as mojibake. Both libraries hand you UTF-8 bytes. The corruption is almost always in how you print them: a Windows console needs its code page set, and a narrow std::string written to a stream is not transcoded for you.
Frequently asked questions
Does the C++ standard library include an XML parser?
No, and no proposal to add one is close to adoption. The standard library covers strings, containers, algorithms and, since C++17, filesystem access. XML, JSON and YAML all require a third party library.
Which XML library is fastest?
RapidXML generally leads on raw parse throughput because it modifies your buffer in place and allocates almost nothing, with pugixml close behind. The tradeoff is that your source buffer must outlive the document. For most applications parsing is not the bottleneck.
Can I read a huge XML file without loading it all into memory?
Yes, but not with a DOM parser. Use a streaming approach: libxml2’s reader API, or pugixml’s xml_document::load_buffer_inplace combined with chunking your own input. Every example in this guide builds a full tree in memory.
How do I write XML rather than read it?
All four libraries support it. In pugixml, build nodes with append_child and append_attribute, then call doc.save_file("out.xml"). In TinyXML2, use NewElement and InsertEndChild followed by SaveFile. If you are new to file output in C++, see our guide on writing a file in C++.
Should I use header only RapidXML or a linked library?
Header only is simpler to vendor and needs no build system changes, which is why RapidXML is attractive for small tools. A linked library keeps compile times down on large codebases because the parser is compiled once rather than in every translation unit that includes it.
The bottom line
Start with pugixml. The install is one package, the API reads like modern C++, and XPath turns most extraction tasks into a single expression. Both examples above compiled cleanly under -Wall -Wextra and ran on the first try.
Move to TinyXML2 only if vendoring two files matters more than the API, to RapidXML only after profiling proves parsing is your bottleneck, and to libxml2 only when you genuinely need schema validation or namespaces. The official pugixml manual is thorough and worth reading once through. For more C++ fundamentals see our guides on storing text file data in a vector and joining two vectors in C++.
