kuniga.me > NP-Incompleteness > Customization Point Objects in C++
10 Sep 2025
In this post I’d like to share my notes on customization point objects (or CPOs) in C++. This is a mechanism that allows users of libraries to provide overloads for their custom types.
Suppose we’re a library provider and we expose an API that can be extended for user-defined types. As a simple example for this post, consider a function print()
which takes a template and internally calls a function getArea()
which users can overload with their own type.
We’ll now consider ways to implement this.
One way to implement this would be via object oriented programming, more specifically via dynamic dispatch. We could define a base class:
struct IShape {
virtual double getArea() = 0;
};
and have a specific type implement getArea()
:
struct Triangle : public IShape {
Triangle(double b, double h) : b_(b), h_(h) {}
double getArea() override {
return h_ * b_ / 2.0;
}
double h_;
double b_;
};
which the print()
function can then call:
void print(const IShape& s) {
std::cout << "area: " << s.getArea() << std::endl;
}
using it:
Triangle t(10, 4);
print(t);
The problem with this approach is that compilers usually implement dynamic dispatch using vtables, which adds overhead to runtime. The use of inheritance might also be problematic if existing code is not using classes, so some wrapping might be needed.
Another option to avoid dynamic dispatch is to use templates. We could have print()
take a template type and assume it implements a method called getArea()
.
template<typename T>
void print(T& obj) {
std::cout << "area: " << obj.getArea() << std::endl;
}
Or using concepts:
template<typename T>
concept HasGetArea = requires (T c) {
c.getArea();
};
template<HasGetArea T>
void print(T& obj) {
std::cout << "area: " << obj.getArea() << std::endl;
}
In this approach we avoid the runtime cost of the dynamic dispatch (moving the cost to compile time), but still require the use of classes.
We can declare getArea()
to be a templated function and require custom types to provide their own implementation.
template<typename T>
double getArea(const T& t);
template<>
double getArea(const Triangle& t) {
return t.b_ * t.h_ / 2.0;
}
The behavior is more explicit. One danger is that a template specialization is considered a different signature than a non-templated function, so the compiler will allow you to “redefine” getArea()
without the template:
double getArea(const Triangle& t) {
return t.b_ * t.h_ / 2.0;
}
and this takes precedence over the template specialization.
Argument-Dependent Lookup, or ADL relies on a complex set of rules the compiler uses to decide overload function to call based on the types and namespaces of the arguments. Continuing with the example, getArea()
can be function that can be overloaded by specific types:
double getArea(const Triangle& t) {
return t.h() * t.b() / 2.0;
}
template<typename T>
void print(T& obj) {
std::cout << "area: " << getArea(obj) << std::endl;
}
Writing a function overload is less intrusive than adding methods to an existing class or wrapping types into classes doing that. Another advantage is that we can also override function overloads, even if an implementation already exists for our type.
A classic example is std::swap()
. It has a default implementation more or less like:
namespace std {
template<typename T>
void swap(T &t1, T &t2) noexcept {
T temp = t1;
t1 = t2;
t2 = temp;
}
}
In Item 25 of Effective C++ [2], Scott Meyers suggest using template specialization of std::swap()
by injecting it into the std
namespace via:
struct Triangle {
...
void swap(C &other) {
cout << "custom swap" << endl;
}
};
namespace std {
template<>
void swap(Triangle &t1, Triangle &t2) noexcept {
t1.swap(t2);
}
}
Turns out this is undefined behavior and highly discouraged, especially after C++20. The alternative is to define the function in your own namespace then calling swap()
unqualified and rely on ADL to choose the desired overload:
namespace geometry {
struct Triangle {...};
...
void swap(Triangle &t1, Triangle &t2) noexcept {
t1.swap(t2);
}
} // geometry
int main () {
using std::swap;
geometry::Triangle t1;
geometry::Triangle t2;
swap(t1, t2); // geometry::swap()
int a = 1;
int b = 2;
swap(a, b); // std::swap()
}
The using std::swap
becomes a fallback in case you call swap()
with types other than Triangle
. The key here is that ADL can call geometry::swap()
even without qualifiers because the argument is from that namespace.
This can lead to ambiguity if the overload is defined in multiple namespaces and the arguments are from different namespaces. A super contrived example:
namespace A {
struct X {};
}
namespace B {
struct Y {};
}
namespace A {
void f(X, B::Y) {
std::cout << "a" << std::endl;
}
}
namespace B {
void f(A::X, Y) {
std::cout << "b" << std::endl;
}
}
int main() {
A::X x;
B::Y y;
f(x, y); // ambiguous call
}
In this case the function takes arguments for namespaces A
and B
and f()
is overloaded in both namespaces.
The post What is ADL? by Arthur O’Dwyer provides more details.
A more modern alternative is the use of Customization Point Objects, or CPO. The library author would declare the function as:
struct getAreaFunctor {
template <typename T>
constexpr auto operator()(T&& t) const
noexcept(noexcept(tag_invoke(*this, std::forward<T>(t))))
-> decltype(tag_invoke(*this, std::forward<T>(t)))
{
return tag_invoke(*this, std::forward<T>(t));
}
};
inline constexpr getAreaFunctor getArea{};
Which looks pretty complicated, but if we strip the code for handling templates and inferring types it boils down to roughly:
struct getAreaFunctor {
auto operator()(T&& t) const noexcept {
return tag_invoke(*this, t);
}
};
If a user wants to provide a custom implementation, they can do:
double tag_invoke(getAreaFunctor, const Triangle& t) {
return t.b_ * t.h_ / 2.0;
}
Note the first argument is of type getAreaFunctor
and since it’s not used we can omit the name. Now we can call the object getArea
as if it was a free function:
template<typename T>
void print(T& obj) {
std::cout << "area: " << getArea(obj) << std::endl;
}
Since the overload requires the unique type getAreaFunctor
, it’s very hard for hijacking to happen accidentaly. With this technique it’s also possible to have an explicit fallback in case users don’t provide an implementation:
template <typename Tag, typename T>
concept tag_invocable =
requires(Tag tag, T&& t) {
tag_invoke(tag, std::forward<T>(t));
};
struct getAreaFunctor {
template <typename T>
requires tag_invocable<getAreaFunctor, T&&>
constexpr auto operator()(T&& t) const
noexcept(noexcept(tag_invoke(*this, std::forward<T>(t))))
-> decltype(tag_invoke(*this, std::forward<T>(t)))
{
return tag_invoke(*this, std::forward<T>(t));
}
template <typename T>
requires (!tag_invocable<getAreaFunctor, T&&>)
constexpr void operator()(T&& t) const noexcept {
std::cout << "Fallback implementation\n";
}
};
It requires using the concept tag_invocable
because otherwise both operator()
overloads would match and the compilation would fail due to ambiguity.
To summarize, the advantages of CPO are:
In this post we covered different alternatives with different tradeoffs. I learned about ADL, CPO and a few syntaxes around templates and type inference such as
noexcept(noexcept(<expr>))
. There are a lot of nuances around ADL which I learned from [4].
I still don’t have a sense on why some alternatives are prefered over others, for example, is template specialization much worse than CPO?
In Effective Modern C++, Scott Meyes [3] presents a technique called Tag Dispatching which resembles CPO in a way. It uses static types, e.g. std::true_type
and std::false_type
to select which overload to invoke:
template<typename T>
void f(T&& x) {
fImpl(
std::forward<T>(x),
std::is_integral<std::remove_ref<T>>
);
}
void fImpl(int x, std::true_type) { }
template<typename T>
void fImpl(T&& x, std::false_type) { }
This however solves a different problem, because here the library implementation f()
would need to be aware of the user types.