/ C++API DESIGN

dont_deduce<T>

template <class T>
struct foo_t 
{
    using type = T;
};

template <class T>
using foo = typename foo_t<T>::type;

Now that’s a pretty useless snippet.

… or is it??

Controlling Type Deduction

Spoiler alert: it’s not useless.

In particular, it allows us to control template argument deduction to a certain extent.

In my libraries, I usually define the typedef as follows:

template <class T>
struct dont_deduce_t 
{
    using type = T;
};

template <class T>
using dont_deduce = typename dont_deduce_t<T>::type;

This clearly communicates our intent: we want to disable type deduction for a certain parameter.

In C++20, the same functionality is provided in <type_traits> under std::type_identity (though I find this name significantly less clear in a function declaration). Also note that I prefer a different convention than the C++ standard: the implementation type ends with _t while the typedef is “clean”.

Okay okay, not so fast. What problem are we trying to solve here?

Motivating Example: Vector Math

template <class T>
struct vec3
{
    T x, y, z;
};

template <class T>
vec3<T> operator*(vec3<T> const& a, T b)
{
    return {a.x * b, a.y * b, a.z * b};
}

That looks like a reasonable definition of operator*, doesn’t it?

Turns out, it doesn’t provide the smooth API that we’d like to have.

vec3<float> v = ...;
v = v * 3; // that'd be a cool API, right?

GCC 10.2 politely refuses this code but not without a proper explanation:

<source>:16:11: error: no match for 'operator*' (operand types are 'vec3<float>' and 'int')
   16 |     v = v * 3;
      |         ~ ^ ~
      |         |   |
      |         |   int
      |         vec3<float>
<source>:8:9: note: candidate: 'template<class T> vec3<T> operator*(const vec3<T>&, T)'
    8 | vec3<T> operator*(vec3<T> const& a, T b)
      |         ^~~~~~~~
<source>:8:9: note:   template argument deduction/substitution failed:
<source>:16:13: note:   deduced conflicting types for parameter 'T' ('float' and 'int')
   16 |     v = v * 3;
      |             ^

What happens is that operator* is called with a vec3<float> and int. The compiler then tries to deduce a T such that the signature (vec3<T> const&, T) is satisfied. For the first argument it figures T = float might be a good match while for the second, T = int is the natural choice. Thus it responds: deduced conflicting types for parameter 'T' ('float' and 'int').

The compiler is only happy if the deductions for all arguments agree. However, our intention was more along the lines of: “Deduce T from vec3<T> const& and then try to convert b to T, preferably with an error if this doesn’t work”.

We could make this work with additional template arguments and SFINAE:

template <class T, class B, std::enable_if_t<std::is_convertible_v<B, T>, int> = 0>
vec3<T> operator*(vec3<T> const& a, B b);

However, in my opinion, the superior solution is to “disable deduction” for b by turning its type into a so called non-deduced context. dont_deduce<T> is not a simple typedef of T. Rather, it is “piped through” the templated class dont_deduce_t<T> (via a typedef ::type that just maps T to itself). Because template specialization can arbitrarily mess with templated classes, deduction does not work “through” dont_deduce_t<T>::type. In particular, just because the compiler sees that dont_deduce_t<T>::type should be float, it cannot deduce that T must be float as well. Just imagine if someone writes a template specialization where dont_deduce_t<some_user_type>::type is float.

Shower thought: How about user-defined per-function deduction guides in C++3x?

Anyways, by using dont_deduce<T> we take away the compiler’s ability to reason about T, allowing us to write a rather clean API:

template <class T>
vec3<T> operator*(vec3<T> const& a, dont_deduce<T> b);

And voilà, now v * 3 just works.

As a non-deduced context, the second argument of operator* is not used for template type deduction. Thus, only vec3<T> const& is matched against the vec3<float>, resulting in an unambiguous T = float. After the typedef is resolved, we have operator*(vec3<float> const&, float) which is called with vec3<float> and int, which is perfectly fine as there obviously is a conversion from int to float.

If the second argument is not convertible (e.g. v * "foo"), we get a nice error message:

<source>:25:11: error: no match for 'operator*' (operand types are 'vec3<float>' and 'const char [4]')
   25 |     v = v * "foo";
      |         ~ ^ ~~~~~
      |         |   |
      |         |   const char [4]
      |         vec3<float>
<source>:17:9: note: candidate: 'vec3<T> operator*(const vec3<T>&, dont_deduce<T>) [with T = float; dont_deduce<T> = float]'
   17 | vec3<T> operator*(vec3<T> const& a, dont_deduce<T> b)
      |         ^~~~~~~~
<source>:17:52: note:   no known conversion for argument 2 from 'const char [4]' to 'dont_deduce<float>' {aka 'float'}
   17 | vec3<T> operator*(vec3<T> const& a, dont_deduce<T> b)
      |                                     ~~~~~~~~~~~~~~~^

This also highlights a subtle difference between the SFINAE and the dont_deduce<T> solution: With SFINAE, the function overload does not really exist (though modern compilers still give reasonable, though often confusing error messages). With dont_deduce<T>, the function exists and it’s like calling a function with the wrong type of parameters.

Also, SFINAE tends to blow up compile times while there should be no measurable negative impact of using dont_deduce<T>.

Other Useful Examples

That’s all that I wanted to explain about dont_deduce<T>. What follows are a few additional examples where it makes for a better API (in my opinion) if some arguments are not deduced.

Clamping

template <class T>
T clamp(T value, dont_deduce<T> min, dont_deduce<T> max)
{
    return value < min ? min : value > max ? max : value;
}

// otherwise this wouldn't work:
float v = ...;
v = clamp(v, 0, 1);

Contains with Epsilon

template <class T>
bool contains(sphere3<T> const& sphere, pos3<T> const& p, dont_deduce<T> eps)
{
    return distance(sphere.center, p) <= sphere.radius + eps;
}

// otherwise this wouldn't work:
sphere3<float> s = ...;
pos3<float> p = ...;
if (contains(s, p, 1e-5)) ...;

Queries with Defaults

template <class T>
T get_property_or(property_handle<T> prop, dont_deduce<T> default_val);

// otherwise this wouldn't work:
property_handle<std::string_view> p;
auto s = get_property_or(p, "<no value>");

Note that in this example the handle should dictate the type, not the default value.

Containers and Spans

template <class T>
void add_range(std::vector<T>& vec, dont_deduce<std::span<T const>> values)
{
    vec.resize(vec.size() + values.size());
    for (auto const& v : values)
        vec.push_back(v);
}

// otherwise this wouldn't work:
std::vector<float> vecA = ...;
std::vector<float> vecB = ...;
add_range(vecA, vecB);

Summary

The (seemingly useless) dont_deduce<T> (or std::type_identity) can be used to selectively disable template argument deduction.

This is a valuable tool for reducing API friction:

template <class T>
vec3<T> operator*(vec3<T> const& a, T b); // (A)

template <class T>
vec3<T> operator*(vec3<T> const& a, dont_deduce<T> b); // (B)

vec3<float> v;
v * 3; // works with (B) but not with (A)

Additional discussion and comments on reddit.

(Title image from pixabay)