Folly.Poly: a library for creating type-erasing polymorphic wrappers
Summary: `Poly` is a class template that makes it relatively easy to define a type-erasing polymorphic object wrapper. == Type-erasure `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_ptr`s and `unique_ptr`s 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)>>`. ) == Example: Defining a type-erasing function wrapper with `folly::Poly` Defining a polymorphic wrapper with `Poly` is a matter of defining two things: * 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()` lang=c++ // 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::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>>; 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: lang=c++ Function<int(int, int)> fun = std::plus<int>{}; assert(5 == fun(2, 3)); fun = std::multiplies<int>{}; assert(6 = fun(2, 3)); == Defining an interface with C++17 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: lang=c++ 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>; 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: lang=c++ 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>; 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`. == Defining an interface with C++14 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: lang=c++ 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>; and lang=c++ 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>; == Extending interfaces 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. lang=c++ 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>; 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 `Extends`: lang=c++ Poly<IDerived> derived = ...; Poly<IBase> base = derived; // This conversion is OK. As you would expect, there is no conversion in the other direction, and at present there is no `Poly` equivalent to `dynamic_cast`. == Type-erasing polymorphic reference wrappers 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. lang=c++ 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)); 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: lang=c++ 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::Foo\n"); } }; SomeFoo foo; Poly<IFoo &> const anyFoo = foo; anyFoo->Foo(); // prints "SomeFoo::Foo" 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 `Poly`s. For instance: lang=c++ 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)); == Non-member functions (C++17) 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: lang=c++ struct ILogicallyNegatable { template <class Base> struct Interface : Base { bool operator!() const { return folly::poly_call<0>(*this); } }; template <class T> using Members = folly::PolyMembers< +[](T const& t) -> decltype(!t) { return !t; }>; }; 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. == Non-member functions (C++14) 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: lang=c++ 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>); }; 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. == Multi-dispatch 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 `Self` template alias to define an interface that takes more than one object of the the erased type. lang=c++ struct IAddable { template <class Base> struct Interface : Base { friend Self<Base> operator+(Self<Base> const& a, Self<Base> const& b) const { return folly::poly_call<0>(a, b); } }; template <class T> using Members = folly::PolyMembers< +[](T const& a, T const& b) -> decltype(a + b) { return a + b; }>; }; Given the above defintion of `IAddable` we would be able to do the following: lang=c++ Poly<IAddable> a = 2, b = 3; Poly<IAddable> c = a + b; assert(poly_cast<int>(c) == 5); If `a` and `b` stored objects of different types, a `BadPolyCast` exception would be thrown. == Move-only types If you want to store move-only types, then your interface should extend the `IMoveOnly` interface. == Implementation notes `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 `double`s. `Poly` objects are always nothrow movable. If you store an object in one that has 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 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. Reviewed By: yfeldblum Differential Revision: D4897112 fbshipit-source-id: ff1c1156316bfbdd8f2205c4f57932c0067cacac
Showing
folly/Poly-inl.h
0 → 100644
folly/Poly.cpp
0 → 100644
folly/Poly.h
0 → 100644
This diff is collapsed.
folly/detail/PolyDetail.h
0 → 100644
This diff is collapsed.
folly/detail/TypeList.h
0 → 100644
This diff is collapsed.
folly/docs/Poly.md
0 → 100644
This diff is collapsed.
folly/poly/Nullable.h
0 → 100644
folly/poly/Regular.h
0 → 100644
folly/test/PolyTest.cpp
0 → 100644
This diff is collapsed.
folly/test/TypeListTest.cpp
0 → 100644
Please register or sign in to comment