Customization Point Objects
This post presents a technique for implementing extendable function objects that work with some common types by default, with the default being overridable. These kind of function objects can be called Customization Point Objects (CPO), an idiom introduced in C++20. This article explains CPOs in detail.
The technique is presented on the example of implementing a display
function object, that is specific to my application. The full implementation is in the end of the post.
Here is how the display
CPO is used in my application:
REQUIRE(display(1.2) == "1.20");
REQUIRE(display(10'000.0, NumberFormat::LossyCompact) == "10.00K");
REQUIRE(display(std::optional(30'000.0), NumberFormat::WithThousandsSeparator) == "30,000.00");
REQUIRE(display(std::optional<double>()) == "<select>");
When extending the CPO for a custom type, it is possible to define many additional parameters in the specialization. In my case, there is NumberFormat
parameter defined only with double
overloads.
The CPO is extendable via specializing DisplayCpo
or providing an ADL-reachable display_adl(T, ...)
function for the T
. The display_adl
is gracefully ignored if the prototype doesn’t match (unlike the simple ADL lookup). If neither is provided for a type, there will be a user friendly error ‘pay_attention_implement_display_cpo’ with the type name included as the first issue in the compiler output.
Specializations can benefit from composition by referring to each other. In this particular case, the specialization for optional is parameterized by the inner type and the else “Some” branch just forwards the implementation to the inner type specialization rather than duplicating the implementation. If the inner type doesn’t provide a specialization, there will be a nice error mentioning the inner type name.
The specializations are overridable as long as they are not the most specialized. In my case it is not impossible to override the specialization for double, but for optional it is possible.
#include <QString>
enum class NumberFormat {
LossyCompact,
WithThousandsSeparator,
};
struct Display {
template <class... Args>
requires(sizeof...(Args) > 0)
QString operator()(Args const&...) const;
};
inline constexpr Display display{};
template <class T>
struct DisplayCpo {
template <class... Args>
static QString apply(Args const&...) {
static_assert(T::pay_attention_implement_display_cpo);
return {};
}
};
template <class... Args>
concept DisplayOverloadExists = requires(Args... args) { display_adl(args...); };
template <class T>
struct WithUnpackedTupleDisplayOverloadExists {
constexpr static bool value = false;
};
template <class... Args>
struct WithUnpackedTupleDisplayOverloadExists<std::tuple<Args...>> {
constexpr static bool value = DisplayOverloadExists<Args...>;
};
template <class T>
concept OverloadFoundViaAdl = WithUnpackedTupleDisplayOverloadExists<T>::value ||
DisplayOverloadExists<T>;
template <OverloadFoundViaAdl T>
struct DisplayCpo<T> {
template <class... Args>
static QString apply(Args const&... args) {
return display_adl(args...);
}
};
template <class T>
concept OptionalLike = (requires(T x) {
{ std::get<0>(x).has_value() };
{ *std::get<0>(x) };
}) || requires(T x) {
{ x.has_value() };
{ *x };
};
template <OptionalLike T>
struct DisplayCpo<T> {
template <class T2, class... Args>
static QString apply(T2 const& x, Args const&... args) {
if (x.has_value()) {
return display(*x, args...);
} else {
return "<select>";
}
}
};
template <>
struct DisplayCpo<double> {
static QString apply(double x) {
return display(x, NumberFormat::LossyCompact);
}
};
template <>
struct DisplayCpo<std::tuple<double, NumberFormat>> {
static QString apply(double x, NumberFormat format) {
switch (format) {
case NumberFormat::LossyCompact:
if (constexpr auto m = 1'000'000; std::abs(x) > m) {
return QString{"%1M"}.arg(x / m, 0, 'f', 2);
} else if (constexpr auto k = 1'000; std::abs(x) > k) {
return QString{"%1K"}.arg(x / k, 0, 'f', 2);
} else {
return QString{"%1"}.arg(x, 0, 'f', 2);
}
case NumberFormat::WithThousandsSeparator:
return QString{"%L1"}.arg(x, 0, 'f', 2);
}
return DEBUG_UNREACHABLE(Abort{});
}
};
template <class... Args>
requires(sizeof...(Args) > 0)
QString Display::operator()(Args const&... args) const {
if constexpr (sizeof...(Args) == 1) {
return DisplayCpo<Args...>::apply(args...);
} else {
return DisplayCpo<std::tuple<Args...>>::apply(args...);
}
}
using namespace std;
int main() {
cout << display(1.2) << endl;
cout << display(10'000.0, NumberFormat::LossyCompact) << endl;
cout << display(std::optional(30'000.0), NumberFormat::WithThousandsSeparator) << endl;
cout << display(std::optional<double>()) << endl;
}
// g++ -std=c++20 foo.cpp -fpic $(pkg-config --cflags --libs Qt6Core)