Delaying function signature instantiation in C++11

I think everyone had enough of rvalue references for now so let’s look at another interesting C++11 technique: delayed function signature instantiation. It is made possible thanks to the default function template arguments.

To understand the motivation behind this technique, let’s first review the various stages of instantiation of a class template. At the first stage all we get is just the template-id. Here is an example:

template <typename T>
class foo;
 
class bar;
 
typedef foo<bar> foo_bar;

At this stage both the template and its type arguments only need to be forward-declared and the resulting template-id can be used in places where the size of a class nor its members need to be known. For example, to form a pointer or a reference:

foo_bar* p = 0;     // ok
void f (foo<bar>&); // ok
foo_bar x;          // error: need size
p->f ();            // error: foo<bar>::f is unknown

In other words, this is the same as forward-declaration for non-template classes.

The last two lines in the above example wouldn’t have been errors if we had defined the foo class template. Instead, it would have triggered the second instantiation stage during which the class definition (i.e., its body) is instantiated. In particular, this includes instantiation of all data members and member function signatures. However, this stage does not involve instantiation of member function bodies. This only happens at the third stage, when we actually use (e.g., call or take a pointer to) specific functions. Here is another example that illustrates all the stages together:

template <typename T>
class foo
{
public:
  void f (T* p)
  {
    delete p_;
    p_ = p;
  }
 
  T* p_;
};
 
class bar;
 
void f (foo<bar>&); // stage 1
foo<bar> x;         // stage 2
x.f ();             // stage 3

While the class template definition is required for the second stage and the function definition is required for the third stage, whether the type template arguments must be defined at any of these stages depends on the template implementation. For example, the foo class template above does not require the template argument to be defined during the second stage but does require it to be defined during the third stage when f()’s body is instantiated.

Probably the best known example of class templates that don’t require the template argument to be defined during the second stage are smart pointers. This is because, like with raw pointers, we often need to form smart pointers to forward-declared types:

class bar;
 
bar* create ();                 // ok
std::shared_ptr<bar> create (); // ok

It is fairly straightforward to implement normal smart pointers like std::shared_ptr in such a way as to not require the template argument to be defined. But here is a problem that I ran into when implementing a special kind of smart pointer in ODB, called a lazy pointer. If you read some of my previous posts you probably remember what a lazy pointer is (it turned out to be a very fertile ground for discovering interesting C++11 techniques). For those new to the idea, here is quick recap: when an object that contains lazy pointers to other objects is loaded from the database, these other objects are not loaded right away (which would be the case for normal, eager pointers such as std::shared_ptr). Instead, just the object ids are loaded and the objects themselves can be loaded later, when and if required.

A lazy pointer can be initialized with an actual pointer to a persistent object, in which case the pointer is said to be loaded. Or we can initialize it with an object id, in which case the pointer is unloaded.

When I first set out to implement a lazy pointer, I naturally added the following extra constructor to support creating unloaded pointers (in reality id_type is not defined by T but rather by odb::object_traits<T>; however this difference is not material to the discussion):

template <class T>
class lazy_shared_ptr
{
  lazy_shared_ptr (database&, const typename T::id_type&);
 
  ...
};

Do you see the problem? Remember that during the second stage function signatures get instantiated. And in order to instantiate the signature of the above constructor, the template argument must be defined, since we are looking for id_type inside this type. As a result, lazy_shared_ptr can no longer be used with forward-declared classes.

As it turns out, we can delay function signature instantiation until the third stage (i.e., when the function is actually used) by making the function itself a template. Here is how we can fix the above constructor so that we can continue using lazy_shared_ptr with forward-declared types. This method works even in C++98:

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <typename ID>
  lazy_shared_ptr (database&, const ID&);
};

As a side note, some of you who read my previous posts about rvalue references were wondering why I used the constructor template here. Well, now you know.

The above C++98-compatible implementation has a number of drawbacks. The biggest is that we cannot use this technique for function return types. In ODB, lazy pointers also allow querying the object id of a stored object. In the C++98 mode, to keep the implementation usable on forward-declared types, I had to resort to this ugly interface:

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <typename T1>
  typename T1::id_type object_id () const;
};
 
lazy_shared_ptr<object> lp = ...
cerr << lp->object_id<object> (); << endl;

That is, the user has to explicitly specify the object type when calling object_id().

The second problem has to do with the looseness of the resulting interface. Now we can pass any value as id when initializing lazy_shared_ptr. While an incompatible type will get caught, it will only happen in the implementation with the resulting diagnostics pointing to the wrong place and saying the wrong thing (we have to provide our own correct “diagnostics” in the comment):

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <typename ID>
  lazy_shared_ptr (database&, const ID& id)
  {
    // Compiler error pointing here? Perhaps the id
    // argument is wrong?
    //
    const typename T::id_type& real_id (id);
    ...
  }
};

Support for default function template arguments in C++11 allows us to resolve both of these problems. Let’s start with the return type:

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <typename T1 = T>
  typename T1::id_type object_id () const;
};
 
lazy_shared_ptr<object> lp = ...
cerr << lp->object_id (); << endl;

The solution to the second problem is equally simple:

template <class T>
class lazy_shared_ptr
{
  ...
 
  template <typename T1 = T>
  lazy_shared_ptr (database&, const typename T1::id_type&);
};

The idea here is to inhibit template argument deduction in order to force the default type to always be used. This is similar to the trick used in std::forward().

One Response to “Delaying function signature instantiation in C++11”

  1. Bryan St. Amour Says:

    Very nice trick! I’m going to keep this in mind when designing my objects to try and keep error messages sane.