Reputation: 45484
I have the following class structure
// file foo.h:
struct foo_base
{ ... }
template<typename T> struct foo : foo_base
{ ... };
template<typename F>
using is_foo = std::is_convertible<F,foo_base>;
template<typename, typename=void> struct aux;
template<typename Foo>
struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>
{ ... }; // specialisation for any foo
// file bar.h:
#include "foo.h"
template<typename T> struct bar : foo<T>
{ ... };
template<typename T>
struct aux<bar<T>>
{ ... }; // specialisation for bar<T>
Now, the problem is that for aux<bar<T>>
both specialisations provided for aux
are viable. Is there a way to avoid this disambiguity without providing another specialisation for every T
? Note that modifications to file foo.h
must not know about file bar.h
.
Note The ambiguity shall be resolved such that the specialisation provided in file bar.h
is picked for any aux<bar<T>>
. Originally, bar
was not a template and the specialisation aux<bar>
not partial and hence preferred. The problem arose by making bar
a template.
Upvotes: 5
Views: 211
Reputation: 70556
Partial specialization of class templates is based on pattern matching. In contrast, customizing function templates is based on template argument deduction and overload resolution.
Because of the class hierarchy present in your problem, customizing behavior would in principle be more convenient through function template overloading, because that can take derived-to-base conversions into account. The pattern matching used in partial class template specialization does not provide the same flexibility.
However, since C++11, it is possible to do compile-time return type deduction. Here is a solution that combines tag dispatching, default constructors and decltype
type deduction:
#include <iostream>
// file foo_base.h:
struct foo_base
{
foo_base() = default;
};
foo_base faux(foo_base const&)
{
return foo_base{};
}
template<class T, class = decltype(faux(T{}))>
struct aux;
template<class T>
struct aux<T, foo_base>
{
enum { value = 1 };
};
// file foo.h:
template<typename T>
struct foo : foo_base
{
foo() = default;
};
// file bar.h:
template<typename T>
struct bar : foo<T>
{
bar() = default;
};
template<class T>
bar<T> faux(bar<T> const&)
{
return bar<T>{};
}
template<class T, class U>
struct aux<T, bar<U>>
{
enum { value = 2 };
};
// file meow.h
template<class T>
struct meow : bar<T>
{
meow() = default;
};
int main()
{
std::cout << aux<foo_base>::value; // 1
std::cout << aux<foo<int>>::value; // 1
std::cout << aux<bar<int>>::value; // 2
std::cout << aux<meow<int>>::value; // 2
}
Live Example that works with both g++ and clang in C++11 mode (C++14 mode is not required!).
The constexpr
function faux()
is overloaded for foo_bar
and as a function template for bar<T>
. Any argument whose class is derived from foo_base
but not from bar<T>
will select the former overload, and anything derived from bar<T>
will select the latter overload. This mechanism is the same as e.g. in the Standard Library where iterator categories are used to tag dispatch several implementations of std::advance()
e.g.
To use this selection mechanism during partial specialization of your class template aux
, two more ingredients are required. First, all classes are required to have a default constructor. Secondly, decltype()
is applied to that expression faux(T{})
to deduce the return type.
NOTE: it is not required that
faux()
isconstexpr
or that any of the default constructors areconstexpr
, becausedecltype()
will not actually evaluate the function call but only deduce its return type.
The main class template aux
has a default template argument:
template<class T, class = decltype(faux(T{}))>
struct aux;
Partially specializing the second argument on foo_base
allows you to provide behavior for any class that derives from foo_base
:
template<class T>
struct aux<T, foo_base>
{ // custom behavior for anything derived from foo_bar };
The second partial specialization matches any class derived from any template instantiation bar<U>
template<class T, class U>
struct aux<T, bar<U>>
{ // custom behavior for anything derived from bar<U> for some U }
NOTE: the main drawback is that you might need to provide a default constructor to all classes in your hierarchy. This may or may not be an obstacle that you can overcome. Most classes already have a default constructor, but some may not. In that sense this solution is intrusive (i.e. it cannot be bolted on top of existing code, but it requires modification of that code).
Upvotes: 2
Reputation:
The compiler doesn't see struct aux<bar<T>>
as more specialised than struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>
because of second template argument. You can specify the second argument the same way in your bar<T>
specialisation:
template<typename T>
struct aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>
{ };
The rules for how specialised partial template specialisations are are complicated, but I will try to explain very briefly:
The three (your two, plus my one) relevant specialisations are
template<typename Foo>
struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>
template<typename T>
struct aux<bar<T>> // or aux<bar<T>, void>
{ };
template<typename T>
struct aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>
{ };
Per the standard (14.5.5.2), to determine which of the class template partial specialisations is the most specialised, the question that needs to be answered is which of the following function template overloads would be the best match in a call to f(aux<bar<T>>())
:
template<typename Foo>
void f(aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>); // 1
template<typename T>
void f(aux<bar<T>>); // or aux<bar<T>, void> // 2
template<typename T>
void f(aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>); // 3
And there, in turn, the partial ordering rules for functions say that 1 is not more specialised than 2, and that 2 is not more specialised than 1, roughly speaking, because 1 is not clearly more specialised than 2, and 2 is not clearly more specialised than 1. "Clearly more specialised" is not how the standard words it, but that essentially means that based on the type arguments of one of those, the type arguments of the other are not deducible.
When comparing 1 and 3, however, the arguments of 1 are deducible from 3: Foo
can be deduced as bar<T>
. Therefore, 3 is at least as specialised as 1. However, the arguments of 3 are not deducible from 1: T
cannot be deduced at all. The compiler's conclusion therefore is that 3 is more specialised than 1.
Upvotes: 4