Smart Pointers

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. 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.

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 for virtual_ptr<std::unique_ptr<Class>>

  • shared_virtual_ptr<Class> is an alias for virtual_ptr<std::shared_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:

  • make_unique_virtual_ptr<Class>(…​)

  • make_shared_virtual_ptr<Class>(…​)

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>. <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
}