Tip of the Week #218: Designing Extension Points With FTADLE

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

Designing Extension Points

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.

Design Goals For Extension Points

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 to 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.

FTADLE: A Good Pattern With A Great Name

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:

  1. 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.
  2. 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.
  3. 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.

Other Examples

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.

What To Avoid

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.

Virtual Functions

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.

Member Functions

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.

Template Specializations

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.

Conclusion

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.

  1. 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 “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. 


Subscribe to the Abseil Blog