string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
Originally posted as TotW #218 on January 19, 2023
By Andy Soffer
Updated 2023-01-19
Quicklink: abseil.io/tips/218
Ftadle. It’s a perfectly cromulent word. ~ Unknown
Suppose you work on a library called sketchy
, that draws on a canvas. You
already provide ways to draw some common things like points, lines, and text,
but you want to provide a mechanism where users can specify how to draw their
own types. You’re designing an extension point.
C++ provides many mechanisms for defining extension points, each with their own benefits and drawbacks. When defining an extension point in C++ there are several considerations worth weighing:
Readability – How easy is it for an engineer to understand the relationship between your library and the extension?
Maintainability – How easy will it be change the extension point as the needs of your library and your library’s users change?
Dependency Hygiene – Does your extension point require your library to be linked in to a user’s binary? We want to make sure extension points play nicely with IWYU, so if a header needs to be included for the extension mechanism to work, the extended types should actually use something from that header.
Lack of ODR violations – Some mechanisms make it easy to have different portions of your program have contradictory views about what a program means. ODR violations are always a bug.
When defining an extension point, we recommend following a pattern that we
lovingly call FTADLE1 (Friend Template
ADL Extension). FTADLE does
well on each of the considerations listed above. It relies heavily on a language
feature known as ADL, or Argument Dependent Lookup; the process by which the
compiler determines what function is intended when a function call appears
without namespace qualification (i.e., no ::
s). ADL is explained in detail in
Tip #49, To write an extension using the FTADLE pattern:
Pick a name for your extension point and prefix it with your project’s
namespace. Our extension is for drawing, and our project lives in the
sketchy
namespace, so we’ll call our extension SketchyDraw
.
Design a type to be passed in to SketchyDraw
that has all the behavior
your users will need. In our case, this is the sketchy::Canvas
on which
users can draw their types.
Implement your functionality as an overload set. One member of that overload
set will be a template and will call your extension point. The non-template
functions in the overload set should be the basic building blocks; the
primitive types that your API supports. In our running example, that means
functions that accept the types sketchy::Point
and sketchy::Line
.
namespace sketchy { // Draws the point `p` on the canvas `c`. void Draw(Canvas& c, const Point& p); // Draws the line segment `l` on the canvas `c`. void Draw(Canvas& c, const Line& l); // For any user-defined type `T` which implements `SketchyDraw` (see // documentation that I've definitely written), draws `value` on the canvas // `c`. template <typename T> void Draw(Canvas& c, const T& value) { // Called without namespace qualifiers. We rely on ADL to find the correct // overload. See [Tip #49]([Tip #49](/tips/49)) for details on ADL. SketchyDraw(c, value); } } // namespace sketchy
With this extension-point designed, users will now be able to make their types
drawable. How can an unrelated type add this Draw
functionality without adding
an explicit dependency? We can make use of a friend function to do so. By adding
a friend function template in their type named SketchyDraw
with the
appropriate signature. the template overload above will use ADL to find the
SketchyDraw
function. For example,
</pre> class Triangle { public: explicit Triangle(Point a, Point b, Point c) : a_(a), b_(b), c_(c) {}
template <typename SC> friend void SketchyDraw(SC& canvas, const Triangle&
triangle) { // Note: This is a template, even though the only type we ever
expect to be // passed in for SC
is sketchy::Canvas
. Using sketchy::Canvas
directly // works, but pulls in an extra dependency that may not be used by all
users // of Triangle
. sketchy::Draw(canvas, sketchy::Line(triangle.a_,
triangle.b_)); sketchy::Draw(canvas, sketchy::Line(triangle.b_, triangle.c_));
sketchy::Draw(canvas, sketchy::Line(triangle.c_, triangle.a_)); }
private: Point a_, b_, c_; };
// Usage: void DrawTriangles(sketchy::Canvas& canvas, absl::Span<const Triangle> triangles) { for (const Triangle& triangle : triangles) { sketchy::Draw(canvas, triangle); } } </pre>
NOTE: Users of the library never call the ADL extension point SketchyDraw
directly. Rather, the library should provide a function like sketchy::Draw
which invokes the extension point on the user’s behalf.
The FTADLE pattern has been used with several other common libraries.
The AbslHashValue
extension point allows you to make your type hashable by
any of Abseil’s hash containers. See Tip #152 for details.
The AbslStringify
extension point allows you to print your type with many
many Abseil libraries, including logging, absl::StrCat
, absl::StrFormat
,
and absl::Substitute
.
Some common extension point mechanisms fall short of our design goals. Virtual functions, checking at compile-time for member functions, and template specialization are each brittle in their own way, as discussed below.
Though perhaps the most familiar, virtual functions and class hierarchies are often overly rigid. They are nearly impossible to refactor, because the base class and all derived classes need to be updated in lock-step. We rarely get designs right on the first try, so it’s prudent to have a design that we can change later.
Beyond the rigidity, class hierarchies force a dependency on your users. In
the case of sketchy
, users are required to depend on sketchy
code, even when
only some of their binaries want to use the dependency. FTADLE ensures that only
binaries that need to do something sketchy
pay that cost.
The same is true for non-template friend extension points as well (for example
std::ostream
’s operator<<
). Each class that wishes to implement operator<<
must include one of the standard library headers defining std::ostream
(e.g.,
<ostream>
, <iostream>
). This means that (barring optimizations) the code for
std::ostream
will be compiled and linked into the binary whether or not
operator<<
is used, a potential extra cost to compile-times and binary size.
With some template trickery, you could check to see if a class has a particular method by name (or even signature). However, names can be misleading.
// Requires that the image have a `draw()` member function. template <typename Image> void DisplayImage(const Image& image) { image.draw(); } class Cowboy { public: // Draws the gun from the holster. void draw(); }; int main() { Cowboy c; DisplayImage(c); // Oops, not the "draw" we meant. }
With the FTADLE pattern, the extension point is prefixed with the project’s namespace, mitigating accidental conformance.
Another common but dangerous technique is to use template specializations. This
is how std::hash
and std::less
are specialized.
namespace std { template<> struct hash<MyType> { size_t operator()(const MyType& m) const { return HashCombine(std::hash<>()(m.foo()), std::hash<>()(m.bar())); } }; } // namespace std
Aside from requiring more boilerplate, this technique is ripe for ODR violations. While not terribly common, providing different specializations for this type in different translation units, or even the same definition twice is an ODR violation. More commonly, if such a specialization is available only in some translation units but not others, metaprogramming techniques will produce different answers to the question “is there a hash function available?” which is also an ODR-violation.
Beyond that, it is generally bad practice to open up a namespace you do not own (amongst other reasons, because it leads to ODR violations). We should design our APIs to avoid bad practices so as not to accidentally encourage dangerous practices.
The FTADLE extension point pattern is readable, maintainable, mitigates against ODR violations, and avoids adding dependencies. If your library needs an extension point, FTADLE comes highly recommended.
C++ has a rich tradition of almost-pronouncable acronyms, including RAII, IFNDR, CRTP, and SFINAE. We have been pronouncing FTADLE as “fftah-dill” (similar to “battle” but with the ‘b’ replaced by the sound at the end of “raft”), but we encourage you to pronounce it in whichever way brings you the most joy. ↩