Interoperation with Boost.TypeErasure
Methods can take
Boost.TypeErasure
anys as virtual parameters, in the same manner as for a plain anys - see
Interoperation with any.
Support is provided by <boost/openmethod/interop/boost_type_erasure.hpp>. It
is not included by <boost/openmethod.hpp>, so it must be included explicitly.
Requirements
Dispatch resolves on the type returned by boost::type_erasure::typeid_of, so
the any's concept must include boost::type_erasure::typeid_<>.
typeid_of returns a std::type_info, so the registry’s rtti policy must be
std_rtti, or a policy derived from it. default_registry and
indirect_registry both qualify. A registry with, say, static_rtti
identifies classes by a different kind of type_id, and would look up the wrong
v-table; the requirement is enforced with a static_assert. The
openmethod_vptr concept takes the v-table pointer from the any's own
dispatch table and never calls typeid_of, so it does not require std_rtti.
The types an any may bind to are registered automatically: naming a type as
the parameter of an overrider registers it as a class derived from the owning
any - the root class for the Concept; storing a value in a virtual_any
(see below) registers its type as well. anys with different Concepts have
distinct roots. A type that is never named in one of these ways is not
registered and cannot be dispatched on - not even by a catch-all overrider: a
call with such a value bound to the any is a missing_class error -
see Error Handling.
The any is wrapped in virtual_ in the method parameters; overriders receive
the bound value - or, for a catch-all overrider, the any itself. In the
example below, int is registered by the weigh overrider, and, having no
name overrider of its own, is dispatched by name to the catch-all:
#include <iostream>
#include <string>
#include <boost/mpl/vector.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/builtin.hpp>
#include <boost/type_erasure/is_empty.hpp>
#include <boost/openmethod.hpp>
#include <boost/openmethod/interop/boost_type_erasure.hpp>
namespace te = boost::type_erasure;
using namespace boost::openmethod;
// `relaxed` implies `typeid_<>`, which dispatch relies on.
using Concept = boost::mpl::vector<te::copy_constructible<>, te::relaxed>;
using erased = te::any<Concept>;
struct Dog {
std::string name;
};
// The owning `any`, `any<Concept>`, is the common base of the types the
// `any` may bind. An overrider registers the type it names as a class
// derived from it.
BOOST_OPENMETHOD(name, (virtual_<const erased&>), std::string);
// An overrider takes the bound value...
BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) {
return dog.name + " the dog";
}
BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) {
return name;
}
// ...or the `any` itself, which makes it a catch-all.
BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) {
return te::is_empty(value) ? "nothing" : "something else";
}
BOOST_OPENMETHOD(weigh, (virtual_<const erased&>), int);
BOOST_OPENMETHOD_OVERRIDE(weigh, (const int& value), int) {
return value;
}
#include <boost/openmethod/initialize.hpp>
int main() {
initialize();
const erased spot(Dog{"Spot"});
const erased felix(std::string("Felix the cat"));
const erased answer(42);
std::cout << name(spot) << "\n"; // Spot the dog
std::cout << name(felix) << "\n"; // Felix the cat
// `int` is registered - `weigh`'s overrider names it - but has no
// `name` overrider of its own, so the catch-all applies.
std::cout << weigh(answer) << "\n"; // 42
std::cout << name(answer) << "\n"; // something else
}
Reference categories
The any is passed by reference, in any of the three categories -
passing it by value would copy the bound value on every call, and is rejected
at compile time. The category determines what the overriders may take:
| Method parameter | Overrider parameter |
|---|---|
|
|
|
|
|
|
The mutable lvalue reference has the same limitation as virtual_<std::any&>:
BOOST_OPENMETHOD_OVERRIDE
cannot locate the method, because nothing binds a temporary any to a mutable
lvalue reference - see the
explanation there. Those overriders are
registered with the core API instead:
using bump_method =
BOOST_OPENMETHOD_TYPE(bump, (virtual_<any<Concept>&>), std::string);
auto bump_dog(Dog& dog) -> std::string {
dog.name += " Jr.";
return dog.name;
}
BOOST_OPENMETHOD_REGISTER(bump_method::override<bump_dog>);
boost::type_erasure::any_cast has no rvalue overload, so, in the rvalue
case, moving the value out of the any is performed by the interop code
itself: an overrider taking Dog&& receives the bound value ready to be moved
from, and the any still owns the moved-from object afterwards.
any references
Boost.TypeErasure also has non-owning any's, any<Concept, self&> and
any<Concept, const _self&>, which hold a _reference to a value stored
elsewhere. They are cheap, two-word handles, and, unlike an owning any, they
are passed by value - the idiomatic way to use them as parameters. Modifications
made through a mutable any reference reach the referent:
#include <iostream>
#include <string>
#include <boost/mpl/vector.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/builtin.hpp>
#include <boost/openmethod.hpp>
#include <boost/openmethod/interop/boost_type_erasure.hpp>
namespace te = boost::type_erasure;
using namespace boost::openmethod;
using Concept = boost::mpl::vector<te::copy_constructible<>, te::relaxed>;
using erased_ref = te::any<Concept, te::_self&>;
struct Dog {
std::string name;
};
// An any reference is a cheap handle; it is passed by value.
BOOST_OPENMETHOD(poke, (virtual_<erased_ref>), std::string);
BOOST_OPENMETHOD_OVERRIDE(poke, (Dog& dog), std::string) {
dog.name += "!";
return dog.name;
}
BOOST_OPENMETHOD_OVERRIDE(poke, (int& value), std::string) {
++value;
return "poked";
}
#include <boost/openmethod/initialize.hpp>
int main() {
initialize();
Dog snoopy{"Snoopy"};
int count = 41;
// mutations reach the referents
std::cout << poke(erased_ref(snoopy)) << "\n"; // Snoopy!
std::cout << snoopy.name << "\n"; // Snoopy!
std::cout << poke(erased_ref(count)) << "\n"; // poked
std::cout << count << "\n"; // 42
}
Dispatch is on the type bound at construction of the any reference - never on
the C++ RTTI dynamic type of the referent. The rvalue-reference placeholder
(_self&&), and placeholders other than _self, are not supported.
For the same reason as for std::any, final_virtual_ptr is deleted
for type_erasure::any: it would silently produce the v-table of the root
class rather than the one for the bound value.
virtual_any
Every call above looks the v-table up in a hash table, keyed on the type the
any binds. virtual_any removes that cost: it bundles an any with the
v-table pointer for the value bound to it, acquiring it once, on construction,
and maintaining it across assignment and emplace. It is to an any what
virtual_ptr is to a pointer, except that it owns the object: the any
is held by value. Building it from a value, or emplace, also registers the
type, like naming it in an overrider does; so does assigning a value. The
any's concept must include relaxed - for the default constructor and
assignment - and copy_constructible<> for copies.
#include <iostream>
#include <string>
#include <boost/mpl/vector.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/builtin.hpp>
#include <boost/openmethod.hpp>
#include <boost/openmethod/interop/boost_type_erasure.hpp>
namespace te = boost::type_erasure;
using namespace boost::openmethod;
using Concept = boost::mpl::vector<te::copy_constructible<>, te::relaxed>;
using erased = te::any<Concept>;
struct Dog {
std::string name;
};
BOOST_OPENMETHOD(name, (const virtual_any<erased>&), std::string);
BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) {
return dog.name + " the dog";
}
BOOST_OPENMETHOD_OVERRIDE(name, (const std::string& name), std::string) {
return name;
}
#include <boost/openmethod/initialize.hpp>
int main() {
initialize();
// from a value: the v-table pointer is set statically
virtual_any<erased> rex = Dog{"Rex"};
std::cout << (rex.vptr() == default_registry::static_vptr<Dog>) << "\n"; // 1
// from an `any`: one lookup, here, and none in the calls below
erased spot_any(Dog{"Spot"});
virtual_any<erased> spot = spot_any;
std::cout << name(rex) << "\n"; // Rex the dog
std::cout << name(spot) << "\n"; // Spot the dog
spot = std::string("Felix the cat");
std::cout << name(spot) << "\n"; // Felix the cat
}
The wrapper is passed by reference, in any of the three categories, and the
overriders receive the bound value, exactly as for virtual_<const
any<Concept>&> - or the wrapper itself, for a catch-all overrider. It works
with type_erasure::any for the same reason it works with std::any and
boost::any - the interop specializes virtual_traits for the reference
types of the any. See Interoperation with any for
the details.
The openmethod_vptr concept
openmethod_vptr is an alternative to virtual_any, pursuing the same
goal - constant-time access to the v-table pointer - from the other side.
Instead of wrapping the any from the outside, it puts the v-table pointer
inside the dispatch table of the any itself, making every any on it
intrinsically polymorphic, and leaving the call sites unchanged. The two are
mutually exclusive: an any that carries the concept cannot be wrapped in a
virtual_any, because the hook returns the v-table pointer by value, and an
indirect registry cannot store that; wrapping one is rejected at compile time.
virtual_any is to an any what virtual_ptr is to a pointer, while
openmethod_vptr is to an any what inplace_vptr_base is to a class -
see Alternatives to virtual_ptr.
Constant-time is not the same as free, though, and the two do not cost the same. Clang compiles a call on the x64 architecture to the following - symbol names are shortened for readability.
openmethod_vptr |
virtual_any |
|---|---|
|
|
The concept reaches the v-table pointer through a call on the any's own
dispatch table - the call is openmethod_vptr::apply, and the register
shuffling around it is there to keep the argument alive across it. A
virtual_any holds the pointer as a member, so acquiring it is the one load at
[rdi + 32].
What the concept removes, relative to a plain any, is therefore the hash of
type_erasure::typeid_of, not the call. Only virtual_any gets down to a
load, and it pays for that in size - the pointer sits next to the any - and
in having to acquire it up front.
openmethod_vptr is implemented via the boost_openmethod_vptr ADL
customization point.
Binding a value to such an any also registers its type, as a class derived
from the owning any - the registrar is instantiated along with the
operation, just as naming the type in an overrider instantiates one; the two
registration paths may coexist.
openmethod_vptr must name the Concept it belongs to, which makes the
definition self-referential. That rules out a type alias: the Concept is a
struct deriving from the mpl::vector, so it can pass its own name - CRTP, in
effect:
#include <iostream>
#include <string>
#include <boost/mpl/vector.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/builtin.hpp>
#include <boost/type_erasure/is_empty.hpp>
#include <boost/openmethod.hpp>
#include <boost/openmethod/interop/boost_type_erasure.hpp>
namespace te = boost::type_erasure;
using namespace boost::openmethod;
struct Dog {
std::string name;
};
struct Dispatchable
: boost::mpl::vector<
te::copy_constructible<>, te::relaxed,
openmethod_vptr<Dispatchable>> {};
using erased = te::any<Dispatchable>;
BOOST_OPENMETHOD(name, (virtual_<const erased&>), std::string);
BOOST_OPENMETHOD_OVERRIDE(name, (const Dog& dog), std::string) {
return dog.name + " the dog";
}
BOOST_OPENMETHOD_OVERRIDE(name, (const erased& value), std::string) {
return te::is_empty(value) ? "nothing" : "something else";
}
#include <boost/openmethod/initialize.hpp>
int main() {
initialize();
const erased spot(Dog{"Spot"});
const erased answer(42);
std::cout << name(spot) << "\n"; // Spot the dog
std::cout << name(answer) << "\n"; // something else
}
The price is coupling: the Concept must be OpenMethod-aware, and the registry
is part of the type of the any - whereas the typeid_of-based dispatch above
works with any pre-existing Concept containing typeid_<>. To use an any
with several registries, list the concept several times, once per registry:
openmethod_vptr<Concept, some_registry>.
In exchange, the requirement for std_rtti goes away. The v-table pointer
comes from the any's own dispatch table, so the registry’s rtti policy is
needed only to identify classes when initialize() builds the dispatch tables -
static_rtti is enough. No hashing is involved either: the registry needs
neither a vptr policy nor any policy that one depends on, like type_hash.
registry<policies::static_rtti> will do. Note that this does not make the
program RTTI-free: Boost.TypeErasure itself uses typeid.
Empty anys
An empty relaxed any reports typeid(void), which is not a registered
class, so dispatching on it is a missing_class error. A catch-all
overrider does not help: dispatch never reaches it. Check with
boost::type_erasure::is_empty before calling. With the openmethod_vptr
concept, the failure mode differs: calling a concept operation on an empty
relaxed any throws boost::type_erasure::bad_function_call.