C++ structured bindings’ power

I’ve recently been polishing my pet-project (dupa – duplicate analyzer). It uses SQLite3 under the hood, so I figured I’d come up with a really simple C++ wrapper around the official SQLite3 C library. Certainly, it is not a full-fledged product, but I think this is how a modern C++ interface to a small database should look like, because you can unleash the expressiveness of C++ structured bindings.

This is an example snippet from dupa showing what I mean:

for (const auto &[path, cksum, size, mtime] :
     db.Query(
         "SELECT path, cksum, size, mtime FROM FileList")) {
  // do stuff with path, cksum, size, mtime
}

The most important thing to me was to make it possible to specify the types of the columns next to the query string and not repeat those type specifications further. This is still not LINQ-smart, but I think it’s as good as it gets in C++.

Here are examples from different approaches (from SQLiteCpp and sqlite3cc):

SQLite::Statement query(db,
    "SELECT path, cksum, size, mtime FROM FileList");
while (query.executeStep())
{
  const std::string &path = query.getColumn(0);
  const Cksum &cksum = query.getColumn(1);
  const off_t &size = query.getColumn(2);
  const time_t &mtime = query.getColumn(3);
  // do stuff with path, cksum, size, mtime
}
for(const auto &i : sqlite::query(conn,
    "SELECT path, cksum, size, mtime FROM FileList")) {
  const std::string &path;
  const Cksum &cksum;
  const off_t &size;
  const time_t &mtime;
  i >> path >> cksum >> size >> mtime;
  // do stuff with path, cksum, size, mtime
}

They respectively overload the cast operators or stream operators. While this allows you to achieve what I wanted to, it doesn’t force you to – you can still accidentally access the same column of a query result as 2 different types or at the very least, makes the code more verbose.

Writing to a database with my library is also C++ish and type-safe (simplified snippet from dupa):

auto out = db.Prepare(
    "INSERT INTO EqClass(id, nodes, weight, interesting) "
    "VALUES(?, ?, ?, 0)");
for (const auto &eq_class : classes) {
  out->Write(reinterpret_cast(eq_class.get()),
                   eq_class->GetNumNodes(), eq_class->GetWeight());
}

Alternatively, if you’re a fan of functional programming, you can write it that way too:

auto out = db.Prepare(
    "INSERT INTO EqClass(id, nodes, weight, interesting) "
    "VALUES(?, ?, ?, 0)");
std::transform(classes.begin(), classes.end(), out->begin(),
               [](const std::unique_ptr &eq_class) {
                 return std::make_tuple(
                     reinterpret_cast(eq_class.get()),
                     eq_class->GetNumNodes(), eq_class->GetWeight());
               });

How it’s achieved? The cornerstone of this SQLite wrapper is the decision that we’ll be binding the types of columns with input or output streams to the database. It is achieved by DBInStream and DBOutStream being variadically templated by the column types. These streams are created on Query and Prepare invocations. An straight-forward implication of this decision is that an iterator over DBInStream has to return a tuple typed the same way as DBInStream. The rest is just C++17 awesomeness. There are some gory details obviously. Take a look at src/db_lib* in dupa and if you have questions, I’ll happily reply.

If you’re interested in moving this database library forward, please let me know and I’ll rip it out and start a new project with it.

Published by

dopiera

Full time geek.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.