C++11 support in ODB
Tuesday, March 27th, 2012One of the major new features in the upcoming ODB 2.0.0 release is support for C++11. In this post I would like to show what is now possible when using ODB in the C++11 mode. Towards the end I will also mention some of the interesting implementation-related issues that we encountered. This would be of interest to anyone who is working on general-purpose C++ libraries or tools that have to be compatible with multiple C++ compilers as well as support both C++98 and C++11 from the same codebase.
In case you are not familiar with ODB, it is an object-relational mapping (ORM) system for C++. It allows you to persist C++ objects to a relational database without having to deal with tables, columns, or SQL, and manually writing any of the mapping code.
While the 2.0.0 release is still a few weeks out, if you would like to give the new C++11 support a try, you can use the 1.9.0.a1 pre-release.
While one could use most of the core C++11 language features with ODB even before 2.0.0, what was lacking is the integration with the new C++11 standard library components, specifically smart pointers and containers. By default, ODB still compiles in the C++98 mode, however, it is now possible to switch to the C++11 mode using the --std c++11
command line option (this is similar to GCC’s --std=c++0x
). As you may remember, ODB uses GCC as a C++ compiler frontend which means ODB has arguably the best C++11 feature coverage available, especially now with the release of GCC 4.7.
Let’s start our examination of the C++11 standard library integration with smart pointers. New in C++11 are std::unique_ptr
and std::shared_ptr
/weak_ptr
. Both of these smart pointers can now be used as object pointers:
#include <memory> class employer; #pragma db object pointer(std::unique_ptr) class employee { ... std::shared_ptr<employer> employer_; }; #pragma db object pointer(std::shared_ptr) class employer { ... };
ODB now also provides lazy variants for these smart pointers: odb::lazy_unique_ptr
, odb::lazy_shared_ptr
, and odb::lazy_weak_ptr
. Here is an example:
#include <memory> #include <vector> #include <odb/lazy-ptr.hxx> class employer; #pragma db object pointer(std::shared_ptr) class employee { ... std::shared_ptr<employer> employer_; }; #pragma db object pointer(std::shared_ptr) class employer { ... #pragma db inverse(employer_) std::vector<odb::lazy_weak_ptr<employee>> employees_; };
Besides as object pointers, unique_ptr
and shared_ptr
/weak_ptr
can also be used in data members. For example:
#include <memory> #include <vector> #pragma db object class person { ... #pragma db type("BLOB") null std::unique_ptr<std::vector<char>> public_key_; };
It is unfortunate that boost::optional
didn’t make it to C++11 as it would be ideal to handle the NULL
semantics (boost::optional
is supported by the Boost profile). The good news is that it seems there are plans to submit an std::optional
proposal for TR2.
The newly supported containers are: std::array
, std::forward_list
, and the unordered containers. Here is an example of using std::unordered_set
:
#include <string> #include <unordered_set> #pragma db object class person { ... std::unordered_set<std::string> emails_; };
One C++11 language feature that comes really handy when dealing with query results is the range-based for
-loop:
typedef odb::query<employee> query; transaction t (db->begin ()); auto r (db->query<employee> (query::first == "John")); for (employee& e: r) cout << e.first () << ' ' << e.last () << endl; t.commit ();
So far we have tested C++11 support with various versions of GCC as well as VC++ 10 (we will also test with Clang before the final release). In fact, all the tests in our test suite build and run without any issues in the C++11 mode with these two compilers. ODB also comes with an example, called c++11
, that shows support for some of the C++11 features discussed above.
These are the user-visible features when it comes to C++11 support and they are nice and neat. For those interested, here are some not so neat implementation details that I think other library authors will have to deal with if they decide to support C++11.
The first issue that we had to address is simultaneous support for C++98 and C++11. In our case, supporting both from the same codebase was not that difficult (though more on that shortly). We just had to add a number of #ifdef ODB_CXX11
.
What we only realized later was that to make C++11 support practical we also had to support both from the same installation. To understand why, consider what happens when a library is packaged, say, for Ubuntu or Fedora. A single library is built and a single set of headers is packaged. To be at all usable, these packages cannot be C++98 or C++11. They have to support both at the same time. It is probably possible to have two versions of the library and ask the user to link to the correct one depending on which C++ standard they are using. But you will inevitably run into tooling limitations (e.g., pkg-config
doesn’t have the --std c++11
option). The situation with headers are even worse, unless your users are prepared to pass a specific -I
option depending on which C++ standard they are using.
The conclusion that we came to is this: if you want your library to be usable once installed in both C++98 and C++11 modes in a canonical way (i.e., without having to specify extra -I
options, defines, or different libraries to link), then the C++11 support has to be header-only.
This has some interesting implications. For example, initially, we used an autoconf
test to detect whether we are in the C++11 mode and write the appropriate value to config.h
. This had to be scraped and we now use a more convoluted and less robust way of detecting the C++ standard using pre-defined compiler macros such as __cplusplus
and __GXX_EXPERIMENTAL_CXX0X__
. The other limitation of this decision is that all “extra” C++11 functions, such as move constructors, etc., have to be inline or templates. While these restrictions sound constraining, so far we didn’t have any serious issues maintaining C++11 support header-only. Things fitted quite naturally into this model but that, of course, may change in the future.
The other issue that we had to deal with is the different level of C++11 support provided by different compiler implementations. While GCC is more or less the gold standard in this regard, VC++ 10 lacked quite a few features that we needed, specifically, deleted functions, explicit conversion operators, and default function template arguments. As a result, we had to introduce additional macros that indicate which C++11 features are available. This felt like early C++98 days all over again. Interestingly, none of the above mentioned three features will be supported in the upcoming VC++ 11. In fact, if you look at the VC++ C++11 support table, it is quite clear that Microsoft is concentrating on the user-facing features, like the range-based for
-loop. This means there will probably be some grief for some time for library writers.