|
proxygen
|
Poly is a class template that makes it relatively easy to define a type-erasing polymorphic object wrapper.
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:
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::PolyDefining a polymorphic wrapper with Poly is a matter of defining two things:
Below is a simple example program that defines a drawable wrapper for any type that provides a draw member function. (The details will be explained later.)
The above program prints:
Here is another (heavily commented) example of a simple implementation of a std::function-like polymorphic wrapper. Its interface has only a single member function: operator()
Given the above definition of Function, users can now initialize instances of (say) Function<int(int, int)> with function objects like std::plus<int> and std::multiplies<int>, as below:
With C++17, defining an interface to be used with Poly is fairly straightforward. 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.
Imagine we were defining something like a Java-style iterator. If we are using a C++17 compiler, our interface would look something like this:
Given the above definition, JavaIterator<int> can be used to hold instances of any type that has Done, Current, and Next member functions with the correct (or compatible) signatures.
The presence of overloaded member functions complicates this picture. Often, property members are faked in C++ with const and non-const member function overloads, like in the interface specified below:
Now, any object that has Value members of compatible signatures can be assigned to instances of IntProperty object. Note how folly::sig is used to disambiguate the overloads of &T::Value.
In C++14, the nice syntax above doesn't work, so we have to resort to macros. The two examples above would look like this:
and
One typical advantage of inheritance-based solutions to runtime polymorphism 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.
Given the above definition, instances of type FooBar have both Foo() and Bar() member functions.
The sensible conversions exist between a wrapped derived type and a wrapped base type. For instance, assuming IDerived extends IBase with PolyExtends:
As you would expect, there is no conversion in the other direction, and at present there is no Poly equivalent to dynamic_cast.
Sometimes you don't need to own a copy of an object; a reference will do. For 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.
A reference-like 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:
Notice in the above code that the 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.
The same mechanism also prevents users from calling non-const member functions on Poly objects that have captured const references, which would violate const-correctness.
Sensible conversions exist between non-reference and reference Polys. For instance:
If you wanted to write the interface ILogicallyNegatable, which captures all 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.
With C++17 you may use a lambda to create a binding, as shown in the example below:
This requires some explanation. The unary operator+ in front of the lambda is 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!.
If you are using a free function to create a binding, the first parameter is implicitly the this parameter. It will receive the type-erased object.
If you are using a C++14 compiler, the definition of ILogicallyNegatable above 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:
As with the example that uses the lambda in the preceding section, the first parameter is implicitly the this parameter. It will receive the type-erased object.
What if you want to create an IAddable interface for things that can be added? 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 the erased type.
Given the above definition of IAddable we would be able to do the following:
If a and b stored objects of different types, a BadPolyCast exception would be thrown.
If you want to store move-only types, then your interface should extend the poly::IMoveOnly interface.
Poly will store "small" objects in an internal buffer, avoiding the cost of of 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 that has a potentially throwing move constructor, 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 compiler accomplishes 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.