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:
-
unique_virtual_ptr<Class>is an alias forvirtual_ptr<std::unique_ptr<Class>> -
shared_virtual_ptr<Class>is an alias forvirtual_ptr<std::shared_ptr<Class>>
Support for boost::intrusive_ptr is also available:
-
boost_intrusive_virtual_ptr<Class>is an alias forvirtual_ptr<boost::intrusive_ptr<Class>>
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 astd::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.
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.