If we want maximum performance, we want to use virtual_ptrs in place of ordinary pointers or references. However, we may also want to use smart pointers for lifetime management; think of std::unique_ptr, std::shared_ptr, or boost::intrusive_ptr.

Does it mean that we have to choose between the performance of virtual_ptr and the convenience of std::unique_ptr? Or carry around both types of smart pointers? Fortunately, no. virtual_ptr can inter-operate with smart pointers.

If virtual_ptr recognizes that its template argument is a smart pointer class, it uses that smart pointer to track the underlying object, instead of a plain pointer. virtual_ptr<const Node> and virtual_ptr<std::unique_ptr<const Node>> both point to a const Node; the former uses a plain const Node* while the latter uses a std::unique_ptr<const Node>. Both carry the same v-table pointer.

Smart virtual_ptrs automatically convert to their non-smart, "plain" counterparts - e.g. from virtual_ptr<std::unique_ptr<const Node>> to virtual_ptr<const Node>. Methods and overriders typically use plain virtual_ptrs, although it is not always the case [1].

The reverse conversion, from plain to smart, does not exist, because it would have the potential to accidentally create smart pointers. Likewise, a smart virtual_ptr can be constructed from a smart pointer, but not directly from a plain reference or pointer.

The library provides aliases for standard smart pointers:

Support for boost::intrusive_ptr is also available:

The standard library provides std::make_unique and std::make_shared to create smart pointers. They are convenient, robust in presence of exceptions, and, in the case of std::shared_ptr, more efficient. OpenMethod provides these counterparts:

Since these functions create the object, they know its exact type with certainty. Thus they don’t need to perform a hash table lookup to find the appropriate v-table; they simply read it from a static variable. As a consequence, they don’t require Class to be polymorphic.

The aliases and the make_* functions are aliased in namespace boost::openmethod::aliases, making it convenient to import constructs that are likely to be used together.

Smart virtual_ptrs are implemented in their own headers, found in the interop subdirectory. For example, support for std::unique_ptr is provided in <boost/openmethod/interop/std_unique_ptr.hpp>, and support for boost::intrusive_ptr is provided in <boost/openmethod/interop/boost_intrusive_ptr.hpp>. <boost/openmethod.hpp> does not include smart pointer headers, so they must be included explicitly.

Here is a variation of the AST example that uses dynamic allocation and unique pointers:

#include <boost/openmethod.hpp>
#include <boost/openmethod/interop/std_unique_ptr.hpp>

using namespace boost::openmethod::aliases;

struct Node {
    virtual ~Node() {}
    virtual int value() const = 0;
};

struct Variable : Node {
    Variable(int value) : v(value) {}
    int value() const override { return v; }
    int v;
};

struct Plus : Node {
    Plus(unique_virtual_ptr<const Node> left, unique_virtual_ptr<const Node> right)
        : left(std::move(left)), right(std::move(right)) {}
    int value() const override { return left->value() + right->value(); }
    unique_virtual_ptr<const Node> left, right;
};

struct Times : Node {
    Times(unique_virtual_ptr<const Node> left, unique_virtual_ptr<const Node> right)
        : left(std::move(left)), right(std::move(right)) {}
    int value() const override { return left->value() * right->value(); }
    unique_virtual_ptr<const Node> left, right;
};

#include <iostream>

BOOST_OPENMETHOD(postfix, (virtual_ptr<const Node> node, std::ostream& os), void);

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (virtual_ptr<const Variable> var, std::ostream& os), void) {
    os << var->v;
}

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (virtual_ptr<const Plus> plus, std::ostream& os), void) {
    postfix(plus->left, os);
    os << ' ';
    postfix(plus->right, os);
    os << " +";
}

BOOST_OPENMETHOD_OVERRIDE(
    postfix, (virtual_ptr<const Times> times, std::ostream& os), void) {
    postfix(times->left, os);
    os << ' ';
    postfix(times->right, os);
    os << " *";
}

BOOST_OPENMETHOD_CLASSES(Node, Variable, Plus, Times);

#include <boost/openmethod/initialize.hpp>

int main() {
    boost::openmethod::initialize();

    auto a = std::make_unique<Variable>(2);
    auto b = std::make_unique<Variable>(3);
    auto c = std::make_unique<Variable>(4);
    auto d = make_unique_virtual<Plus>(std::move(a), std::move(b));
    auto e = make_unique_virtual<Times>(std::move(d), std::move(c));

    postfix(e, std::cout);
    std::cout << " = " << e->value() << "\n"; // 2 3 + 4 * = 20
}

Weak Pointers

A std::weak_ptr observes an object without keeping it alive. Since the object may be gone, there is nothing to dispatch on: a weak pointer cannot be used in a virtual parameter, and neither can a virtual_ptr to a weak pointer. Still, an object that is tracked by weak pointers - in a cache, an observer list, or a back pointer - is typically an object that methods will be called on, once a weak pointer has been locked.

  • weak_virtual_ptr<Class> tracks an object with a std::weak_ptr, and remembers its v-table pointer

A weak_virtual_ptr is a storage facility, not a virtual_ptr. It is constructed from a shared_virtual_ptr (or from a std::shared_ptr or a std::weak_ptr), and it remembers the v-table pointer along with the weak pointer. It cannot be dereferenced. Its lock function returns a shared_virtual_ptr, which can be passed to methods. Since the v-table pointer is copied, not looked up, lock costs no more than std::weak_ptr::lock. It returns an empty shared_virtual_ptr if the object no longer exists.

shared_virtual_ptr<Animal> animal = make_shared_virtual<Dog>();
weak_virtual_ptr<Animal> observer = animal;

std::cout << poke(observer.lock()) << "\n"; // bark

animal = nullptr;
std::cout << std::boolalpha << observer.expired() << "\n"; // true

Remembering the v-table pointer is safe with respect to the lifetime of the object, because a std::weak_ptr keeps the control block alive: once the object has been destroyed, the weak pointer stays expired, and the v-table pointer can never be applied to another object. As for any virtual_ptr, the v-table pointer is invalidated if initialize is called again, unless the registry uses the indirect_vptr policy.

A weak_virtual_ptr converts to a weak_virtual_ptr to a base class, but not to a plain or a shared virtual_ptr. A cast to a derived class requires the object: use lock, then cast. Since it is not a virtual_ptr, a weak_virtual_ptr cannot be used in a virtual parameter, but it can be passed to a method as an ordinary parameter. Support for std::weak_ptr is provided in <boost/openmethod/interop/std_weak_ptr.hpp>, which also includes the std::shared_ptr header.


1. For example, consider a transpose method for matrices. If the matrix is symmetric, the overrider should return its argument. This can be implemented by passing a virtual_ptr<std::shared_ptr<const Matrix>> to the method.