Enhex
Enhex

Reputation: 157

Order independent variadic template base specialization

On GCC/Clang The following code doesn't compile:

#include <iostream>
#include <type_traits>
using namespace std;

template<typename X, typename T, typename ...Ts>
void v(X& x, T& value, Ts&... args)
{
    v(x, value);
    v(x, args...);
}

template<typename X, typename T>
enable_if_t<is_integral_v<T>>
v(X& x, T& value) {
    cout << value << endl;
}

template<typename X, typename T>
enable_if_t<is_floating_point_v<T>>
v(X& x, T& value) {
    cout << value << endl;
}

int main() {
    float f = 5.5;
    int i = 2;
    v(f, i, i);
}

Giving the error:

> prog.cc: In instantiation of 'void v(X&, T&, Ts& ...) [with X = float; T = int; Ts = {}]':
> prog.cc:8:3:   required from 'void v(X&, T&, Ts& ...) [with X = float; T = int; Ts = {int}]'
> prog.cc:27:14:   required from here
> prog.cc:9:3: error: no matching function for call to 'v(float&)'
>   v(x, args...);
>   ~^~~~~~~~~~~~
> prog.cc:6:6: note: candidate: template<class X, class T, class ... Ts> void v(X&, T&, Ts& ...)
>  void v(X& x, T& value, Ts&... args)
>       ^
> prog.cc:6:6: note:   template argument deduction/substitution failed:
> prog.cc:9:3: note:   candidate expects at least 2 arguments, 1 provided
>   v(x, args...);
>   ~^~~~~~~~~~~~

If the variadic function is defined after all the base function specializations are defined, the code compiles, but then you get this very undesirable constraint that makes code break with you include things in the wrong order.

Is there a way to add specializations to the variadic base function in a way that doesn't depend on definition order?

Upvotes: 0

Views: 120

Answers (1)

max66
max66

Reputation: 66230

I see some problems in your code.

First and most impostant: you give the same name (v()) to functions that are doing two completely different things; the first v() call a printing function over the first argument and call recursively itself for the following arguments. Tho other v() are printing (SFINAE selected) functions.

Suggestion: use different names; in the following example I've used foo() for the recursive function and bar() for the printing function.

Second: your first argument (X & x) is unused.

Suggestion: remove it.

Third: your function are receiving not-const references but they don't modify the values.

Suggestion: receive the arguments as const references; so you can call also v(5.5f, 3L) (you can't call this way with not-const references)

Following this suggestions, your printing functions become

template <typename T>
std::enable_if_t<std::is_integral<T>{}> bar (T const & value)
 { std::cout << "integral case: " << value << std::endl; }

template <typename T>
std::enable_if_t<std::is_floating_point<T>{}> bar (T const & value)
 { std::cout << "floating case: " << value << std::endl; }

and the recursive function (adding the ground case)

// ground case
void foo ()
 { }

// recursion case
template <typename T, typename ... Ts>
void foo (T const & value, Ts const & ... args)
 {
    bar(value);
    foo(args...);
 }

The following is a full working example

#include <iostream>
#include <type_traits>

template <typename T>
std::enable_if_t<std::is_integral<T>{}> bar (T const & value)
 { std::cout << "integral case: " << value << std::endl; }

template <typename T>
std::enable_if_t<std::is_floating_point<T>{}> bar (T const & value)
 { std::cout << "floating case: " << value << std::endl; }

// ground case
void foo ()
 { }

// recursion case
template <typename T, typename ... Ts>
void foo (T const & value, Ts const & ... args)
 {
    bar(value);
    foo(args...);
 }

int main ()
 {
   float f {5.5};
   int i   {2};

   foo(f, i, i, 3L, 6.6f);
}

-- EDIT --

The OP say

The main problem is still left unsolved - bars have to be defined before foo.

If you accept that bars become static methods in a struct, I propose the following bar struct

struct bar
 {
   template <typename T>
   static std::enable_if_t<std::is_integral<T>{}> func (T const & value)
    { std::cout << "integral case: " << value << std::endl; }

   template <typename T>
   static std::enable_if_t<std::is_floating_point<T>{}> func (T const & value)
    { std::cout << "floating case: " << value << std::endl; }
 };

and foo become

// ground case

template <typename>
void foo ()
 { }

// recursion case
template <typename Bar, typename T, typename ... Ts>
void foo (T const & value, Ts const & ... args)
 {
   Bar::func(value);
   foo<Bar>(args...);
 }

and is called as follows

foo<bar>(f, i, i, 3L, 6.6f);

The following is a full working example where the bar struct is defined after foo()

#include <iostream>
#include <type_traits>

// ground case
template <typename>
void foo ()
 { }

// recursion case
template <typename Bar, typename T, typename ... Ts>
void foo (T const & value, Ts const & ... args)
 {
   Bar::func(value);
   foo<Bar>(args...);
 }

struct bar
 {
   template <typename T>
   static std::enable_if_t<std::is_integral<T>{}> func (T const & value)
    { std::cout << "integral case: " << value << std::endl; }

   template <typename T>
   static std::enable_if_t<std::is_floating_point<T>{}> func (T const & value)
    { std::cout << "floating case: " << value << std::endl; }
 };

int main ()
 {
   float f {5.5};
   int i   {2};

   foo<bar>(f, i, i, 3L, 6.6f);
}

Upvotes: 1

Related Questions