Use Cases of Variable Templates in C++

Since C++14 variables can also be templatized. A variable template defines a family of variable (when declared at namespace scope) or a family of static data members (when defined at class scope). The question is, what is the benefit of variable templates?

I have seen the example of defining PI as a variable template quite often. Here it is from cppreference:

template<class T>
constexpr T pi = T(3.1415926535897932385L);  // variable template
 
template<class T>
T circular_area(T r)                         // function template
{
    return pi<T> * r * r;                    // pi<T> is a variable template instantiation
}

This is not necessarily a bad example but I think more complex examples would be helpful to better understand the use of variable templates. I’ll try to share such examples here.

Use case: reducing code

The stduuid library provides a cross-platform implementation for universally unique identifiers (uuids or guids). It features a function called to_string() that returns a string representation of a uuid, such as "47183823-2574-4bfd-b411-99ed177d3e43". Recently, I have optimized the implementation of this function for speed. At the first attempt, this function looked as follows:

template<class CharT = char,
         class Traits = std::char_traits<CharT>,
         class Allocator = std::allocator<CharT>>
inline std::basic_string<CharT, Traits, Allocator> to_string(uuid const& id)
{
   CharT uustr[] = "00000000-0000-0000-0000-000000000000";
   constexpr CharT encode[] = "0123456789abcdef";

   for (size_t i = 0, index = 0; i < 36; ++i)
   {
      if (i == 8 || i == 13 || i == 18 || i == 23)
      {
         continue;
      }
      uustr[i] = encode[id.data[index] >> 4 & 0x0f];
      uustr[++i] = encode[id.data[index] & 0x0f];
      index++;
   }

   return uustr;
}

You can create a uuid from a string and then convert back to string as follows:

auto guid = uuids::uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value();
REQUIRE(uuids::to_string(guid) == "47183823-2574-4bfd-b411-99ed177d3e43");

This works fine, but if you try to convert to a string of wide characters, then it no longer works:

auto guid = uuids::uuid::from_string("47183823-2574-4bfd-b411-99ed177d3e43").value();
REQUIRE(uuids::to_string<wchar_t>(guid) == L"47183823-2574-4bfd-b411-99ed177d3e43");

This snippet produces the following errors in VC++:

1>test_uuid.cpp
1>uuid.h(614,1): error C2440: 'initializing': cannot convert from 'const char [37]' to 'CharT []'
1>        with
1>        [
1>            CharT=wchar_t
1>        ]
1>uuid.h(614,21): message : The type of the string literal is not compatible with the type of the array
1>test_uuid.cpp(191): message : see reference to function template instantiation 'std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>> uuids::to_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t>>(const uuids::uuid &)' being compiled
1>uuid.h(615,1): error C2440: 'initializing': cannot convert from 'const char [17]' to 'const CharT []'
1>        with
1>        [
1>            CharT=wchar_t
1>        ]
1>uuid.h(615,32): message : The type of the string literal is not compatible with the type of the array
1>uuid.h(615,32): error C2131: expression did not evaluate to a constant
1>uuid.h(615,32): message : a non-constant (sub-)expression was encountered

A possible solution to fix this problem is to specialize to_string() for wchar_t. This can be done as follows:

template<>
inline std::wstring to_string(uuid const& id)
{
   wchar_t uustr[] = L"00000000-0000-0000-0000-000000000000";
   constexpr wchar_t encode[] = L"0123456789abcdef";

   for (size_t i = 0, index = 0; i < 36; ++i)
   {
      if (i == 8 || i == 13 || i == 18 || i == 23)
      {
         continue;
      }
      uustr[i] = encode[id.data[index] >> 4 & 0x0f];
      uustr[++i] = encode[id.data[index] & 0x0f];
      index++;
   }

   return std::wstring(uustr);
}

This works fine, the compiler errors are gone and the program runs as expected.

However, we now have identical duplicate code. The only difference is the initialization of the uustr and encode arrays. This kinda defeats the purpose of template to avoid repetitive code. We can fix this, with the help of variable templates. Here is how:

First, we will define two variable templates as follows:

namespace detail
{
   template <typename CharT>
   constexpr CharT empty_guid[37] = "00000000-0000-0000-0000-000000000000";

   template <typename CharT>
   constexpr CharT guid_encoder[17] = "0123456789abcdef";
}

We will need to specialize them for the wchar_t type so that we can initialize them with wide string literals. This is also done in the detail namespace (not shown below):

template <>
constexpr wchar_t empty_guid<wchar_t>[37] = L"00000000-0000-0000-0000-000000000000";

template <>
constexpr wchar_t guid_encoder<wchar_t>[17] = L"0123456789abcdef";

Having these, we can change the implementation of the to_string() function template as follows:

template<class CharT = char,
         class Traits = std::char_traits<CharT>,
         class Allocator = std::allocator<CharT>>
inline std::basic_string<CharT, Traits, Allocator> to_string(uuid const & id)
{
   std::basic_string<CharT, Traits, Allocator> uustr{detail::empty_guid<CharT>};

   for (size_t i = 0, index = 0; i < 36; ++i)
   {
      if (i == 8 || i == 13 || i == 18 || i == 23)
      {
         continue;
      }
      uustr[i] = detail::guid_encoder<CharT>[id.data[index] >> 4 & 0x0f];
      uustr[++i] = detail::guid_encoder<CharT>[id.data[index] & 0x0f];
      index++;
   }

   return uustr;
}

We only have a primary template, there is no need for the explicit full specialization for wchar_t. We do have that for the variable templates, so there is some duplicate code, but the implementation of to_string() (which contains much more code) is no longer duplicated.

Use case: improve readability

Apart from this, there is also the use case of simplifying the code when using type traits. Type traits that help us query properties of types provide a data member Boolean called value, which is initialized to true or false depending on the property of a type. Here is an example:

template <typename T>
struct is_floating_point
{
   static constexpr bool value = false;
};

template <>
struct is_floating_point<float>
{
   static constexpr bool value = true;
};

template <>
struct is_floating_point<double>
{
   static constexpr bool value = true;
};

template <>
struct is_floating_point<long double>
{
   static constexpr bool value = true;
};

There is a primary template and three specializations for float, double, and long doble. We can use this type trait to constrain the template arguments of a function as follows:

template <typename T>
requires is_floating_point<T>::value
auto add(T a, T b)
{
   return a + b;
}

However, this requires the use of the verbose ::value which can lead to hard to read code in many cases. This is where a variable template can help improve readability. We can define one as follows:

template <typename T>
constexpr bool is_floating_point_v = is_floating_point<T>::value;

And we can use it as shown below:

template <typename T>
requires is_floating_point_v<T>
auto add(T a, T b)
{
   return a + b;
}

Starting with C++17, the standard defines variable templates like this for all the type traits from the standard library. There is, of course, a std::is_floating_point class template, and a std::is_floating_point_v variable template.

Conclusion

In conclusion, variable templates are an important feature in the language to help us reduce template code and improve their readability.

If you have other good examples for using variable templates please share them in the comments.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.