Skip to main content

The ubiq trick

ubiq-trick

First things first: I want to make clear that I did not come up with this. However, I got inspired by Antony Polukhin, who gave a talk at meetingcpp 2018 about his Boost.Pfr library. There is some voodoo-tmp-blackmagic going on in there in order to implement reflections without macros nor external tooling within C++14, so check it out.

A motivating example

Coincidentally, this trick was just recently used by Peter Dimov on the C++ slack chat after Will Wray posted a motivating challenge: Implement the metafunction nttp_t in the following code:

template<int> struct int_constant {};
template<char> struct char_constant {};

template<template<auto> typename T>
using nttp_t = /* TODO: Make this int for T=int_constant. */

It should evaluate to int if we pass int_constant to it and char if we pass char_constant to it.

There are several ways to implement this. Probably anyone who has worked with a bit of template trickery before would be able to somehow hardcode this, but we really want it to work in a more generic environment. Just think of a my_absurd_constant that was declared later, right after the nttp_t. You certainly can't hardcode that in a reliable way. (Note that my_absurd_constant can take only primitive types as non-type template parameter, though, as of pre-C++20)

The solution

Functions have template argument deduction (mostly because you don't want to provide the template arguments for i.e. operator<< when you print something), so a first idea can be to use a rather crippled 'type function' match:

template<auto X, template<auto> class T>
auto match(T<X>) -> decltype(X);

As we can deduce all template arguments just with the one parameter of type T<X>, this should actually work out just fine: decltype(match(int_constant<42>{})).

And it does work out just fine as it evaluates to type int, which is precisely what we want!

In order to "invoke" match now with some arbitrary Trait, we can utilize Antony's "ubiq-trick" to get the type out of the templated value parameter. First, we define us a struct ubiq like so:

inline constexpr struct ubiq
{
  template<class T>
  constexpr operator T () const
  { return T{}; }
} ubiq_;

The struct itself does nothing, however, it has one special property: A constexpr, templated conversion operator, which means whenever we use ubiq_ it can be implicitly converted to any default constructible type T. Thus, it can also be converted to the type of the value which is anonymous within the template argument list of Trait. This is actually the reason why we need all this trickery, because we can't just simply do template<template<auto X> class Trait> using nttp_t = decltype(X)! (try it) Finally, we are able to define nttp_t with it:

template<template<auto> class Trait>
using nttp_t = decltype(match(Trait<ubiq_>{}));

Full code

this->~dasnacl();