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()
vector.at()
auto
for Variable DeclarationsOriginally 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:
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:
sketchy
namespace, so we’ll call our extension SketchyDraw
.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](/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,
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); } }
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.
AbslHashValue
extension point allows you to make your type hashable by
any of Abseil’s hash containers. See Tip #152 for details.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-pronounceable acronyms, including RAII, IFNDR, CRTP, and SFINAE. We have been pronouncing FTADLE as “fftah-dill” (similar to “paddle” but with the ‘p’ replaced by the sound at the end of “raft”), but we encourage you to pronounce it in whichever way brings you the most joy. ↩