How to write specialized variadic template for a mix of const and non const types

Viewed 87

I need to specialize a variadic template when the types are a bunch of std::vector<T>. Here's what I have so far

#include <iostream>

#include <vector>
#include <type_traits>

template<typename... Ts>
struct is_vector{
    is_vector(Ts&...){};
    static void print() {std::cout << "not vectors" <<std::endl;}
};

template<typename... Ts>
struct is_vector<std::vector<Ts>...>
{   is_vector(std::vector<Ts>&...){}
    static void print() {std::cout << "vectors" << std::endl;}
};




int main()
{
    
    auto const a3 = std::vector<double> {10., 20., 30.};
    auto a4 = std::vector<int> { 1,2,3};
    
    auto z = is_vector(a4,a3); z.print(); // print "not vectors"
    auto x = is_vector(a4,a4); x.print(); // print "vectors"
    
    return 0;
}

The problem is that when the arguments to is_vector contains a mix of const and not const vectors, it uses the default class rather than the specialized class. Of course I can write a specialization that will apply if all vectors are const, but how to handle a mix is the issue. I have tried this

template<typename... Ts>
struct is_vector<typename std::remove_const<std::vector<Ts> >::type...>
{   is_vector(std::vector<Ts>&...){}
    static void print() {std::cout << "vectors" << std::endl;}
};

but the compiler (Apple clang version 11.0.3 (clang-1103.0.32.62) Target: x86_64-apple-darwin21.4.0) throws this error

test.cpp:13:8: error: class template partial specialization contains a template parameter that cannot be deduced; this partial specialization will never be used [-Wunusable-partial-specialization] struct is_vector<typename std::remove_conststd::vector<Ts>::type...> ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ test.cpp:12:22: note: non-deducible template parameter 'Ts' template<typename... Ts> ^ 1 error generated.

2 Answers

Separate concerns. Get is_vector working for one input, const or otherwise. Build the variadic version from this.

For example,

template<class T>
struct is_vector_struct
    : std::false_type {};

template<class T, class allocator>
struct is_vector_struct<std::vector<T, allocator>>
    : std::true_type {};

template<class T>
constexpr bool is_vector = is_vector_struct<std::remove_cvref_t<T>>::value;

static_assert(is_vector<std::vector<int> const&>);

Use fold expressions for multiple arguments:

template<class...Ts>
constexpr bool all_are_vectors = (is_vector<Ts> && ...);

If you can't use C++17, you can do this using recursion instead.


Edit: I gave you a type-predicate, but you also want to dispatch. Here are the techniques I know to dispatch based on a predicate.

You can use

  • if constexpr
  • requires (C++20),
  • SFINAE/enable_if (what range v3 does to simulate requires),
  • tag dispatch (what standard algorithms + iterators do) .

If you want to make decision inside a function based on types, like in your example, I think if constexpr is the path of least resistance:

template<class...Ts>
void message(Ts...xs) {
  if constexpr(all_are_vectors<Ts...>)
    std::cout << "all vectors";
  else 
    std::cout << "not all vectors";
}

If you want to selectively add or remove something from an overload set, if constexpr won't help you. This is what requires or enable_if are for.

template<class...Ts>
void message(Ts...) { std::cout << "default message"; }

template<class T, std::size_t N>
void message(std::array<T,N>) { std::cout << "one array"; } 

template<class...Ts>
requires all_are_vectors<Ts...>
void message(Ts...) { std::cout << "all vectors"; }

Because requires is a language-level feature, the compiler will try to prioritize the overload with the most specific requires. With enable_if and variadics, you need to help the compiler out a bit:

template<class...Ts,
         class = std::enable_if_t<!all_are_vectors<Ts...>>>
void message(Ts...) { std::cout << "default message"; }

template<class...Ts,
         class = std::enable_if_t<all_are_vectors<Ts...>>>
void message(Ts...) { std::cout << "all vectors"; }

Then there's tag dispatch. If you have a way of getting a dummy tag type that describes your inputs, you can use regular function overloading to dispatch on that. The advantage here over enable_if or if constexpr is you can have more specific tag types inherit from more general. For example, std::random_access_iterator_tag inherits from std::forward_iterator_tag. Here's how it might look in this case:

struct default_tag {};

struct all_vectors_tag{};

template<class...Ts>
void message_dispatch(default_tag, Ts...) { 
  std::cout << "not all vectors"; 
} 

template<class...Ts>
void message_dispatch(all_vectors_tag, Ts...) {
  std::cout << "all vectors"; 
} 

template<class...Ts>
using get_tag = 
   std::conditional_t<all_are_vectors<Ts...>, 
                      all_are_vectors_tag, 
                      default_tag>;

template<class...Ts>
void message(Ts...xs) { 
  message_dispatch(get_tag<Ts...>{}, xs...); 
}

With C++20 concepts you could go about it like so:

#include <iostream>
#include <vector>
#include <type_traits>

template<typename T>
concept Vector = std::is_same_v<
    std::remove_cvref_t<T>,
    std::vector<typename T::value_type, typename T::allocator_type>
>;

template<typename... Ts>
struct is_vector {
    is_vector(Ts&...) {}
    static void print() {std::cout << "not vectors" <<std::endl;}
};

template<Vector... Ts>
struct is_vector<Ts...> {   
    is_vector(Ts&...) {}
    static void print() {std::cout << "vectors" << std::endl;}
};

int main()
{
    auto const a3 = std::vector<double> {10., 20., 30.};
    auto a4 = std::vector<int> { 1,2,3};
    
    static_assert(Vector<decltype(a3)>);
    static_assert(Vector<decltype(a4)>);
    static_assert(!Vector<int>);

    auto z = is_vector(a4,a3); z.print(); // prints "vectors"
    auto x = is_vector(a4,a4); x.print(); // prints "vectors"
    
    return 0;
}

Note that, depending on the circumstances, you could also take the opportunity to switch on any container that operates like a std::vector by using a broader concept that matches the subset of of the std::vector api your code uses. That would definitely be the more idiomatic approach.

Related