Poly is a class template that makes it relatively easy to define a type-erasing polymorphic object wrapper.
Declared in <folly/Poly.h>
template<class I>
struct Poly final
: /* implementation-defined */
std::function is one example of a type-erasing polymorphic object wrapper;folly::exception_wrapper is another. Type-erasure is often used as an alternative to dynamic polymorphism via inheritance-based virtual dispatch. The distinguishing characteristic of type-erasing wrappers are:
Duck typing: Types do not need to inherit from an abstract base class in order to be assignable to a type-erasing wrapper; they merely need to satisfy a particular interface.
Value semantics: Type-erasing wrappers are objects that can be passed around by value. This is in contrast to abstract base classes which must be passed by reference or by pointer or else suffer from slicing, which causes them to lose their polymorphic behaviors. Reference semantics make it difficult to reason locally about code.
Automatic memory management: When dealing with inheritance-based dynamic polymorphism, it is often necessary to allocate and manage objects on the heap. This leads to a proliferation of shared_ptrs and unique_ptrs in APIs, complicating their point-of-use. APIs that take type-erasing wrappers, on the other hand, can often store small objects in-situ, with no dynamic allocation. The memory management, if any, is handled for you, and leads to cleaner APIs: consumers of your API don't need to pass shared_ptr<AbstractBase>; they can simply pass any object that satisfies the interface you require. (std::function is a particularly compelling example of this benefit. Far worse would be an inheritance-based callable solution like shared_ptr<ICallable<void(int)>>. )
folly::PolyPoly is a matter of defining twothings:
An interface, consisting of public member functions, and
A mapping from a concrete type to a set of member function bindings.
Below is a (heavily commented) example of a simple implementation of a std::function-like polymorphic wrapper. Its interface has only a single member function: operator()
// An interface for a callable object of a particular signature, Fun // (most interfaces don't need to be templates, FWIW). template <class Fun> struct IFunction;
template <class R, class... As> struct IFunction<R(As...)> { // An interface is defined as a nested class template called // Interface that takes a single template parameter, Base, from // which it inherits. template <class Base> struct Interface : Base { // The Interface has public member functions. These become the // public interface of the resulting Poly instantiation. // (Implementation note: Poly<IFunction<Sig>> will publicly // inherit from this struct, which is what gives it the right // member functions.) R operator()(As... as) const { // The definition of each member function in your interface will // always consist of a single line dispatching to // folly::poly_call<N>. The "N" corresponds to the N-th member // function in the list of member function bindings, Members, // defined below. The first argument will always be *this, and the // rest of the arguments should simply forward (if necessary) the // member function's arguments. return static_cast<R>( folly::poly_call<0>(*this, std::forward<As>(as)...)); } };
// The "Members" alias template is a comma-separated list of bound // member functions for a given concrete type "T". The // "FOLLY_POLY_MEMBERS" macro accepts a comma-separated list, and the // (optional) "FOLLY_POLY_MEMBER" macro lets you disambiguate overloads // by explicitly specifying the function signature the target member // function should have. In this case, we require "T" to have a // function call operator with the signature R(As...) const. // // If you are using a C++17-compatible compiler, you can do away with // the macros and write this as: // // template <class T> // using Members = folly::PolyMembers< // folly::sig<R(As...) const>(&T::operator())>; // // And since folly::sig is only needed for disambiguation in case of // overloads, if you are not concerned about objects with overloaded // function call operators, it could be further simplified to: // // template <class T> // using Members = folly::PolyMembers<&T::operator()>; // template <class T> using Members = FOLLY_POLY_MEMBERS( FOLLY_POLY_MEMBER(R(As...) const, &T::operator())); };
// Now that we have defined the interface, we can pass it to Poly to // create our type-erasing wrapper: template <class Fun> using Function = Poly<IFunction<Fun>>;
Function, users can now initialize instancesof (say) Function<int(int, int)> with function objects like std::plus<int> and std::multiplies<int>, as below:
Function<int(int, int)> fun = std::plus<int>{}; assert(5 == fun(2, 3)); fun = std::multiplies<int>{}; assert(6 = fun(2, 3));
Poly is fairlystraightforward. As in the Function example above, there is a struct with a nested Interface class template and a nested Members alias template. No macros are needed with C++17.
using a C++17 compiler, our interface would look something like this:
template <class Value> struct IJavaIterator { template <class Base> struct Interface : Base { bool Done() const { return folly::poly_call<0>(*this); } Value Current() const { return folly::poly_call<1>(*this); } void Next() { folly::poly_call<2>(*this); } }; // NOTE: This works in C++17 only: template <class T> using Members = folly::PolyMembers<&T::Done, &T::Current, &T::Next>; };
template <class Value> using JavaIterator = Poly<IJavaIterator>;
JavaIterator<int> can be used to hold instancesof any type that has Done, Current, and Next member functions with the correct (or compatible) signatures.
property members are faked in C++ with const and non-const member function overloads, like in the interface specified below:
struct IIntProperty { template <class Base> struct Interface : Base { int Value() const { return folly::poly_call<0>(*this); } void Value(int i) { folly::poly_call<1>(*this, i); } }; // NOTE: This works in C++17 only: template <class T> using Members = folly::PolyMembers< folly::sig<int() const>(&T::Value), folly::sig<void(int)>(&T::Value)>; };
using IntProperty = Poly<IIntProperty>;
Value members of compatible signatures can beassigned to instances of IntProperty object. Note how folly::sig is used to disambiguate the overloads of &T::Value.
The two examples above would look like this:
template <class Value> struct IJavaIterator { template <class Base> struct Interface : Base { bool Done() const { return folly::poly_call<0>(*this); } Value Current() const { return folly::poly_call<1>(*this); } void Next() { folly::poly_call<2>(*this); } }; // NOTE: This works in C++14 and C++17: template <class T> using Members = FOLLY_POLY_MEMBERS(&T::Done, &T::Current, &T::Next); };
template <class Value> using JavaIterator = Poly<IJavaIterator>;
struct IIntProperty { template <class Base> struct Interface : Base { int Value() const { return folly::poly_call<0>(*this); } void Value(int i) { return folly::poly_call<1>(*this, i); } }; // NOTE: This works in C++14 and C++17: template <class T> using Members = FOLLY_POLY_MEMBERS( FOLLY_POLY_MEMBER(int() const, &T::Value), FOLLY_POLY_MEMBER(void(int), &T::Value)); };
using IntProperty = Poly<IIntProperty>;
is that one polymorphic interface could extend another through inheritance. The same can be accomplished with type-erasing polymorphic wrappers. In the Poly library, you can use folly::PolyExtends to say that one interface extends another.
struct IFoo { template <class Base> struct Interface : Base { void Foo() const { return folly::poly_call<0>(*this); } }; template <class T> using Members = FOLLY_POLY_MEMBERS(&T::Foo); };
// The IFooBar interface extends the IFoo interface struct IFooBar : PolyExtends<IFoo> { template <class Base> struct Interface : Base { void Bar() const { return folly::poly_call<0>(*this); } }; template <class T> using Members = FOLLY_POLY_MEMBERS(&T::Bar); };
using FooBar = Poly<IFooBar>;
FooBar have both Foo() andBar() member functions.
base type. For instance, assuming IDerived extends IBase with PolyExtends:
Poly<IDerived> derived = ...; Poly<IBase> base = derived; // This conversion is OK.
present there is no Poly equivalent to dynamic_cast.
that you can use Poly to capture a reference to an object satisfying an interface rather than the whole object itself. The syntax is intuitive.
int i = 42; // Capture a mutable reference to an object of any IRegular type: Poly<IRegular &> intRef = i; assert(42 == folly::poly_cast<int>(intRef)); // Assert that we captured the address of "i": assert(&i == &folly::poly_cast<int>(intRef));
Poly has a different interface than a value-like Poly.Rather than calling member functions with the obj.fun() syntax, you would use the obj->fun() syntax. This is for the sake of const-correctness. For example, consider the code below:
struct IFoo { template <class Base> struct Interface { void Foo() { folly::poly_call<0>(*this); } }; template <class T> using Members = folly::PolyMembers<&T::Foo>; };
struct SomeFoo { void Foo() { std::printf("SomeFoo::Foon"); } };
SomeFoo foo; Poly<IFoo &> const anyFoo = foo; anyFoo->Foo(); // prints "SomeFoo::Foo"
Foo member function is non-const.Notice also that the anyFoo object is const. However, since it has captured a non-const reference to the foo object, it should still be possible to dispatch to the non-const Foo member function. When instantiated with a reference type, Poly has an overloaded operator-> member that returns a pointer to the IFoo interface with the correct const-ness, which makes this work.
const memberfunctions on Poly objects that have captured const references, which would violate const-correctness.
Polys. Forinstance:
Poly<IRegular> value = 42; Poly<IRegular &> mutable_ref = value; Poly<IRegular const &> const_ref = mutable_ref;
assert(&poly_cast<int>(value) == &poly_cast<int>(mutable_ref)); assert(&poly_cast<int>(value) == &poly_cast<int>(const_ref));
ILogicallyNegatable, which capturesall types that can be negated with unary operator!, you could do it as we've shown above, by binding &T::operator! in the nested Members alias template, but that has the problem that it won't work for types that have defined unary operator! as a free function. To handle this case, the Poly library lets you use a free function instead of a member function when creating a binding.
below:
struct ILogicallyNegatable { template <class Base> struct Interface : Base { bool operator!() const { return folly::poly_call<0>(*this); } }; template <class T> using Members = folly::PolyMembers< +[]const& t) -> decltype(!t) { return !t; }>; };
operator+ in front of the lambdais necessary! It causes the lambda to decay to a C-style function pointer, which is one of the types that folly::PolyMembers accepts. The decltype in the lambda return type is also necessary. Through the magic of SFINAE, it will cause Poly<ILogicallyNegatable> to reject any types that don't support unary operator!.
implicitly the this parameter. It will receive the type-erased object.
ILogicallyNegatableabove will fail because lambdas are not constexpr. We can get the same effect by writing the lambda as a named free function, as show below:
struct ILogicallyNegatable { template <class Base> struct Interface : Base { bool operator!() const { return folly::poly_call<0>(*this); } };
template <class T> static auto negate(T const& t) -> decltype(!t) { return !t; }
template <class T> using Members = FOLLY_POLY_MEMBERS(&negate<T>); };
parameter is implicitly the this parameter. It will receive the type-erased object.
IAddable interface for things that can beadded? Adding requires two objects, both of which are type-erased. This interface requires dispatching on both objects, doing the addition only if the types are the same. For this we make use of the PolySelf template alias to define an interface that takes more than one object of the erased type.
struct IAddable { template <class Base> struct Interface : Base { friend PolySelf<Base, Decay> operator+(PolySelf<Base> const& a, PolySelf<Base> const& b) { return folly::poly_call<0, IAddable>(a, b); } };
template <class T> using Members = folly::PolyMembers< +[]const& a, T const& b) -> decltype(a + b) { return a + b; }>; };
IAddable we would be able to do the following:Poly<IAddable> a = 2, b = 3; Poly<IAddable> c = a + b; assert(poly_cast<int>(c) == 5);
a and b stored objects of different types, a BadPolyCast exceptionwould be thrown.
IMoveOnly interface.
Poly will store "small" objects in an internal buffer, avoiding the cost ofof dynamic allocations. At present, this size is not configurable; it is pegged at the size of two doubles.
Poly objects are always nothrow movable. If you store an object in one thathas a potentially throwing move contructor, the object will be stored on the heap, even if it could fit in the internal storage of the Poly object. (So be sure to give your objects nothrow move constructors!)
Poly implements type-erasure in a manner very similar to how the compileraccomplishes virtual dispatch. Every Poly object contains a pointer to a table of function pointers. Member function calls involve a double- indirection: once through the v-pointer, and other indirect function call through the function pointer.
| Name | Description |
|---|---|
/* implementation-defined */ |
| Name | Description |
|---|---|
Poly [constructor] | Construct an empty Poly. |
| Name |
|---|
Unnamed using |
operator= |
| Name | Description |
|---|---|
operator!= | Tests two Poly objects for inequality. |
operator< | Orders two Poly objects. |
operator<= | Orders two Poly objects. |
operator== | Equality operator |
operator> | Orders two Poly objects. |
operator>= | Orders two Poly objects. |
poly_move | ///////////////////////////////////////////////////////////////////////////// |
swap | Swap two Poly<I> instances. |