Weird nested class partial specialization results on both gcc and clang
Asked Answered
H

1

24

While writing a small template metaprogramming library for personal use, I came across an interesting problem.

Since I was reusing a few partial specializations for some metafunctions, I decided I would put them under a common template class and use tags along with nested partial specialization to provide the differences in behaviour.

The problem is I am getting nonsensical (to me) results. Here is a minimal example that showcases what I am trying to do:

#include <iostream>
#include <cxxabi.h>
#include <typeinfo>

template <typename T>
const char * type_name()
{
    return abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
}

template <typename... Args>
struct vargs {};

namespace details   
{
    template <typename K>
    struct outer
    {
        template <typename Arg>
        struct inner
        {
            using result = Arg;
        };
    };
}

struct tag {};

namespace details
{
    template <>
    template <typename Arg, typename... Args>
    struct outer<tag>::inner<vargs<Arg, Args...>>
    {
        using result = typename outer<tag>::inner<Arg>::result;
    };
}

template <typename T>
using test_t = typename details::outer<tag>::inner<T>::result;

int main()
{
    using t = test_t<vargs<char, int>>;
    std::cout << type_name<t>() << '\n';
    return 0;
}

I am getting vargs<char, int> as output when using the 5.1.0 version of gcc and tag when using the 3.6.0 version of clang. My intention was for the above piece of code to print char so I am pretty baffled by these results.

Is the above piece of code legal or does it exhibit undefined behavior? If it's legal what is the expected behavior according to the standard?

Hoban answered 13/7, 2015 at 21:8 Comment(9)
My GCC and Clang results are the reverse of yours. vargs<char, int> is presumably due to the partial specialization not matching, for whatever reason. tag...sounds like a bug.Lenity
@Lenity fixed, thanks for noticing.Hoban
It appears that Clang comes up with details::outer<tag>::inner<tag> during the process somewhere. Interesting, as I have no clue how a compiler can confuse those template arguments.Asgard
Slight modifications lead to an ICE in Clang: coliru.stacked-crooked.com/a/5ae1c222d3d9ad30Asgard
Interestingly, if you completely specialize outer<tag> with inner's default and specialized forms, it produces the expected output: ColiruPeggie
VS 2015 prints char.Hepzi
You can get rid of the variadic template and just use vargs<char> and still get the same issue in clang and gccCountershading
A slightly simpler, but equally effective, change similar to that of @Peggie is to add definition for outer<tag>::inner before attempting to specialize it. See this on IdeoneCountershading
The other issue I ran into is that specializing for composite type e.g. std::vector<T> for outer class seems to be completely impossible according to all the compilers. Though I guess seeing how this technique already super inconsistent I guess I will abstain from using it anyway.Atharvaveda
R
8

Your code is correct; out-of-class implicitly instantiated class template member class template partial specializations are intended to be allowed by the Standard, as long as they are defined early enough.

First, let's try for a minimal example - noting by the way that there's nothing here that requires C++11:

template<class T> struct A {
  template<class T2> struct B { };
};
// implicitly instantiated class template member class template partial specialization
template<> template<class T2>
  struct A<short>::B<T2*> { };
A<short>::B<int*> absip;    // uses partial specialization?

As noted elsewhere MSVC and ICC use the partial specialization as expected; clang selects the partial specialization but messes up its type parameters, aliasing T2 to short instead of int; and gcc ignores the partial specialization entirely.

Why out-of-class implicitly instantiated class template member class template partial specialization is allowed

Put simply, none of the language that permits other forms of class template member class template definitions excludes out-of-class implicitly instantiated class template member class template partial specialization. In [temp.mem], we have:

1 - A template can be declared within a class or class template; such a template is called a member template. A member template can be defined within or outside its class definition or class template definition. [...]

A class template partial specialization is a template declaration ([temp.class.spec]/1). In the same paragraph, there is an example of out-of-class nonspecialized class template member class template partial specialization ([temp.class.spec]/5):

template<class T> struct A {
  struct C {
    template<class T2> struct B { };
  };
};
// partial specialization of A<T>::C::B<T2>
template<class T> template<class T2>
  struct A<T>::C::B<T2*> { };
A<short>::C::B<int*> absip; // uses partial specialization

There is nothing here to indicate that the enclosing scope cannot be an implicit specialization of the enclosing class template.

Similarly, there are examples of in-class class template member class template partial specialization and out-of-class implicitly instantiated class template member class template full specialization ([temp.class.spec.mfunc]/2):

template<class T> struct A {
  template<class T2> struct B {}; // #1
  template<class T2> struct B<T2*> {}; // #2
};
template<> template<class T2> struct A<short>::B {}; // #3
A<char>::B<int*> abcip; // uses #2
A<short>::B<int*> absip; // uses #3
A<char>::B<int> abci; // uses #1

(clang (as of 3.7.0-svn235195) gets the second example wrong; it selects #2 instead of #3 for absip.)

While this does not explicitly mention out-of-class implicitly instantiated class template member class template partial specialization, it does not exclude it either; the reason it isn't here is that it's irrelevant for the particular point being made, which is about which primary template or partial template specializations are considered for a particular specialization.

Per [temp.class.spec]:

6 - [...] when the primary template name is used, any previously-declared partial specializations of the primary template are also considered.

In the above minimal example, A<short>::B<T2*> is a partial specialization of the primary template A<short>::B and so should be considered.

Why it might not be allowed

In other discussion we've seen mention that implicit instantiation (of the enclosing class template) could result in implicit instantiation of the definition of the primary template specialization to take place, resulting in an ill-formed program NDR i.e. UB; [templ.expl.spec]:

6 - If a template, a member template or a member of a class template is explicitly specialized then that specialization shall be declared before the first use of that specialization that would cause an implicit instantiation to take place, in every translation unit in which such a use occurs; no diagnostic is required. [...]

However, here the class template member class template is not used before it is instantiated.

What other people think

In DR1755 (active), the example given is:

template<typename A> struct X { template<typename B> struct Y; };
template struct X<int>;
template<typename A> template<typename B> struct X<A>::Y<B*> { int n; };
int k = X<int>::Y<int*>().n;

This is considered problematic only from the point of view of the existence of the second line instantiating the enclosing class. There was no suggestion from the submitter (Richard Smith) or from CWG that this might be invalid even in the absence of the second line.

In n4090, the example given is:

template<class T> struct A {
  template<class U> struct B {int i; }; // #0
  template<> struct B<float**> {int i2; }; // #1
  // ...
};
// ...
template<> template<class U> // #6
struct A<char>::B<U*>{ int m; };
// ...
int a2 = A<char>::B<float**>{}.m; // Use #6 Not #1

Here the question raised is of precedence between an in-class class template member class template full specialization and an out-of-class class template instantiation member class template partial specialization; there is no suggestion that #6 would not be considered at all.

Rusk answered 21/7, 2015 at 15:25 Comment(4)
I'm under the distinct impression that you wrote a really good answer, but the presence of all these very long phrases (like "out-of-class implicitly instantiated class template member class template partial specialization") prevent my feeble human brains from fully parsing and understanding your answer. Is there a way to make the text a bit more accessible? Could you perhaps start with some well-explained definitions and then use abbreviations for the remainder of the post?Rhebarhee
@Julian, I too would appreciate something like that. My gut feeling, based on my own experiments, is that it's down to the general rule that you can't specialize a template before providing (or at least declaring) the non-specialised template. The compilers don't quite recognize the last template code as a specialization, and nor do they recognize that the first template code provides the non-specialized inner...(to be continued)...Countershading
..(continued).. I think this is relevant because if I add a few lines (lines 32-37 of this on ideone), then both compilers behave correctly; those lines provided an unspecialised outer<tag>::inner. (Well, obviously here, outer is specialized to outer<tag> - but inner itself is non-specialised in this). I think this helps both compilers to understand that the templates are specializations. Anyway, maybe my intuition is wrong!Countershading
@AaronMcDaid what you're doing there is redefining the class template outer<tag>::inner (as a member of the instantiation outer<tag>); but the act of implicitly instantiating outer<tag> should (per [temp.inst]) cause a declaration of outer<tag>::inner, which makes it available for partial specialization. Note that clang selects the partial specialization, it just gets confused over its template arguments.Rusk

© 2022 - 2024 — McMap. All rights reserved.