0

Sorry for the lack of a better title.

While trying to implement my own version of std::move and understanding how easy it was, I'm still confused by how C++ treats partial template specializations. I know how they work, but there's a sort of rule that I found weird and I would like to know the reasoning behind it.

template <typename T>
struct BaseType {
    using Type = T;
};

template <typename T>
struct BaseType<T *> {
    using Type = T;
};

template <typename T>
struct BaseType<T &> {
    using Type = T;
};

using int_ptr = int *;
using int_ref = int &;

// A and B are now both of type int
BaseType<int_ptr>::Type A = 5;
BaseType<int_ref>::Type B = 5;

If there wasn't no partial specializations of RemoveReference, T would always be T: if I gave a int & it would still be a int & throughout the whole template.

However, the partial specialized templates seem to collapse references and pointers: if I gave a int & or a int * and if those types match with the ones from the specialized template, T would just be int.

This feature is extremely awesome and useful, however I'm curious and I would like to know the official reasoning / rules behind this not so obvious quirk.

João Pires
  • 927
  • 1
  • 5
  • 16
  • Unclear. You need to provide a [mcve], that demonstrates what you're talking about. – Sam Varshavchik Sep 24 '17 at 22:30
  • 1
    I'm not entirely sure what the question is. Are you asking basically how template partial specialization works in general? Nothing is "collapsing" references/pointers, it's just doing what you asked it to do. – Barry Sep 24 '17 at 22:53
  • Does it help to point out that `int*` matches `T*`, with `T=int` – Barry Sep 24 '17 at 22:56

2 Answers2

1

If your template pattern matches T& to int&, then T& is int&, which implies T is int.

The type T in the specialization only related to the T in the primary template by the fact it was used to pattern match the first argument.

It may confuse you less to replace T with X or U in the specializations. Reusing variable names can be confusing.

template <typename T>
struct RemoveReference {
  using Type = T;
};

template <typename X>
struct RemoveReference<X &> {
  using Type = X;
};

and X& matches T. If X& is T, and T ia int&, then X is int.


Why does the standard say this?

Suppose we look af a different template specialization:

template<class T>
struct Bob;

template<class E, class A>
struct Bob<std::vector<E,A>>{
  // what should E and A be here?
};
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I believe I did not make my question clear, what I want to know is why when I call `X` inside the 2nd template, it just give me the type but not the rest of it, like the `&`, while in the 1st one `T` gives me the fully qualified type. If I didn't knew about the 2nd one, I could assume by looking that `X` would always be `X &` in the whole template and not just `X`. Like, I'm perfectly okay with it. I just want to know why they did it that way. If i didn't knew nothing about it, I could have seen the 2nd one as - `X` matches the type the guy wants: `X &`, so `X` will be `X &` here. – João Pires Sep 24 '17 at 23:35
0

Partial specializations act a lot like function templates: so much so, in fact, that overloading function templates is often mistaken for partial specialization of them (which is not allowed). Given

template<class T>
void value_assign(T *t) { *t=T(); }

then obviously T must be the version of the argument type without the (outermost) pointer status, because we need that type to compute the value to assign through the pointer. We of course don't typically write value_assign<int>(&i); to call a function of this type, because the arguments can be deduced.

In this case:

template<class T,class U>
void accept_pair(std::pair<T,U>);

note that the number of template parameters is greater than the number of types "supplied" as input (that is, than the number of parameter types used for deduction): complicated types can provide "more than one type's worth" of information.

All of this looks very different from class templates, where the types must be given explicitly (only sometimes true as of C++17) and they are used verbatim in the template (as you said).

But consider the partial specializations again:

template<class>
struct A;                               // undefined
template<class T>
struct A<T*> { /* ... */ };             // #1
template<class T,class U>
struct A<std::pair<T,U>> { /* ... */ }; // #2

These are completely isomorphic to the (unrelated) function templates value_assign and accept_pair respectively. We do have to write, for example, A<int*> to use #1; but this is simply analogous to calling value_assign(&i): in particular, the template arguments are still deduced, only this time from the explicitly-specified type int* rather than from the type of the expression &i. (Because even supplying explicit template arguments requires deduction, a partial specialization must support deducing its template arguments.)

#2 again illustrates the idea that the number of types is not conserved in this process: this should help break the false impression that "the template parameter" should continue to refer to "the type supplied". As such, partial specializations do not merely claim a (generally unbounded) set of template arguments: they interpret them.

Yet another similarity: the choice among multiple partial specializations of the same class template is exactly the same as that for discarding less-specific function templates when they are overloaded. (However, since overload resolution does not occur in the partial specialization case, this process must get rid of all but one candidate there.)

Davis Herring
  • 36,443
  • 4
  • 48
  • 76