How to restrict typenames in template

Have a function which can accept 4 different types. And their implementations are very similar.

template< typename T >
void foo( std::string &&name, T value ){
    //...
}

typename T must be one of 4 predefined types. Other typenames is not acceptable, and should be restricted at compile-time.
Is it possible to write in C++?


I can think of at least three ways, off the top of my head.

First:

namespace internal {
template< typename T >
void foo_impl( std::string &&name, T value ) {
    // actual implementation here
}
}

void foo(std::string &&name, SpecialType1 value ) {
    internal::foo_impl(std::forward<std::string>(name), value);
}
void foo(std::string &&name, SpecialType2 value ) {
    internal::foo_impl(std::forward<std::string>(name), value);
}

Second:

template< typename T >
typename std::enable_if<std::is_same<T, SpecialType1>::value ||
                        std::is_same<T, SpecialType2>::value >::type
foo( std::string &&name, T value ){
    //implementation here
}

Third:

template< typename T >
void foo(std::string &&name, T value){
    static_assert(std::is_same<T, SpecialType1>::value ||
                  std::is_same<T, SpecialType2>::value,
    "wrong template argument");
    //implementation here
}

Demo


<type_traits> let you generalize your logic into class template. How this works is we take template parameter T and parameter pack of Ts and we start checking if T is same to Head of the list of types. If match is found, we inherit from std::true_type and we are done. If no match is found, we pop the head and recursively instantiate same template with tail of Ts. Eventually, if no match is found at all, parameter pack size will drop to zero and the compiler will instantiate base template class that inherit from std::false_type. Please check this video for much better and in dept explanation by Mr. Walter E. Brown.

template<class T, class...> struct is_any_of: std::false_type{};
template<class T, class Head, class... Tail> 
struct is_any_of<T, Head, Tail...>: std::conditional_t<
  std::is_same<T, Head>::value, 
  std::true_type,
  is_any_of<T, Tail...>>
{};

Now we can SFINAE over, using enable_if in almost English wording.

#include <type_traits>
#include <string>

template<
  class T,
  class = std::enable_if_t<is_any_of<T, int, float, unsigned, double>::value>
>
void foo(std::string &&str, T value) {}


int main()
{
  foo(std::string{"hello"}, 3);
  foo(std::string{"world"}, '0'); //compile-time error
}

SFANIE is a language feature, a tool, that's used or abused some say to achieve what you ask for,

The standard library component std::enable_if allows for creating a substitution failure in order to enable or disable particular overloads based on a condition evaluated at compile time. FYI http://en.cppreference.com/w/cpp/language/sfinae.

Note that std::conditional_t<> and std::enable_if_t<> are shorthanded from std::conditional<>::type and std::enable_if<>::type respectively. You could simple replace these in code, but should put typename keyword before enable_if then.


One thing I have seen people done is using std::enable_if. I don't know exactly what your 4 types are, so this is an example with two types int and float.

using myType = std::enable_if<
std::is_same<int, T>::value ||
std::is_same<float, T>::value, T >::type;

myType exists only if T is exactly int or float. Hope that helps!


I know I am late to the party but maybe someone will find this useful.

As of C++20 one can use constraints and concepts to filter possible typenames in a template.

The syntax goes like this:

template <typename T>
requires std::is_integral_v<T>
auto func(T any_integer) {
  ...
};

// Or, you can also do:

template <typename T>
concept Integer = std::is_integral_v<T>;

template <Integer T>
auto func(T any_integer) {
  ...
};

// Or, an even better approach:

auto func(Integer auto any_integer) {
  ...
};

I prefer the last one since it's nice and clean. But, that's just my opinion.

Following are some valid calls to func():

func(int{});         // OK!
func(long{});        // OK!
func(bool{});        // Yes, it's OK too!
func(char{});        // Yes, it's OK too!

Now, some invalid calls to func():

func(double{});      // Compiler error!
func(std::string{}); // Compiler error!

The compiler will also give a clean error message depending on your usage:

note: candidate: ‘template requires is_integral_v void func(T)’

or

note: candidate: ‘template requires Integer auto func(T)’

or

note: candidate: ‘template requires Integerauto:15 auto func(auto:15)’

References:

  1. https://en.cppreference.com/w/cpp/types/is_integral