19

This is probably only a syntax problem.

So i have this template class :

template <typename String, template<class> class Allocator>
class basic_data_object
{
  template<typename T>
  using array_container = std::vector<T, Allocator<T>>;
};

And another one :

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value
{
};

Now i want to specialize the second one's T parameter with the first one's inner typedef array_container for any given type.

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value
<String, Allocator,
typename basic_data_object<String, Allocator>::template array_container<T>>
{
};

But this specialization doesn't seem to be matched when i pass an std::vector as the last parameter.

If i create a temporary hard coded typedef:

typedef basic_data_object<std::string, std::allocator<std::string>> data_object;

And use it for the specialization, everything works :

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value
<String, Allocator,
data_object::template array_container<T>>
{
};

What did i miss ? :)


Alternatively what is the best (smallest / cleanest) way to make this work ?

Drax
  • 11,247
  • 5
  • 37
  • 77
  • Which compiler are you using? – Bathsheba Apr 29 '14 at 10:49
  • Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn) - so basically clang 3.4 i guess :) – Drax Apr 29 '14 at 10:50
  • +1; you have me stumped. I suspected a missing `typename` but that doesn't seem to be the case here. – Bathsheba Apr 29 '14 at 10:51
  • 3
    The short code that reproduces the problem outputs "specialization" only in [vc++2013](http://rextester.com/GUC17610). Both [gcc 4.8.1](http://rextester.com/QFLZIG32455) and [clang 3.4](http://rextester.com/EDWPM93123) outputs "general". – Constructor Apr 29 '14 at 11:16
  • 4
    Looks like a non-deduced context, though I'd guess that compilers should then emit a warning. AFAIK, template type parameters are deduced independently, and you can't deduce `String` and `Allocator` from the last argument. – dyp Apr 29 '14 at 11:42
  • @dyp But both of them also appear as first two arguments as I can see. – Constructor Apr 29 '14 at 11:59
  • 2
    @Constructor True, so you could manually make it a two-step deduction by using a nested template. However I think being a non-deduced context simply disables deduction for the third parameter, even if it could be deduced using the first two. – dyp Apr 29 '14 at 12:31
  • Is there any way to make it work (the nested template stuff might be interesting) ? – Drax Apr 29 '14 at 14:50
  • Well, I give up: http://coliru.stacked-crooked.com/a/3670b6413e6c26be – Mooing Duck Apr 29 '14 at 19:10
  • @MooingDuck Instantiations of alias templates are not treated as .. well .. instantiations of alias templates. They're treated as the types they alias. Therefore AFAIK your `is_instantiation_of` doesn't work as expected. – dyp Apr 29 '14 at 23:16
  • I do not see the problem... Code seems to work: http://ideone.com/qmfIMb – Danvil May 02 '14 at 11:19
  • @Danvil The code compiles, but doesn't work as expected. – Constructor May 02 '14 at 12:02
  • 1
    I reduced the [issue](http://ideone.com/K18pLl) further: removing `template class Allocator` from everywhere and yet it still fails; thus `Allocator` is not part of the problem. – Matthieu M. May 02 '14 at 15:07
  • 1
    Jonathan Wakely nailed the issue you are facing on the head! Regarding finding an actual solution though, I am afraid we have too few elements. I suggests that you ask a separate question, providing more *context* and specifically what is the problem those classes are supposed to solve, why is the specification required, and what classes we can (and cannot) modify to assist you. Maybe with a more complete view, we could provide a more elegant solution as well. – Matthieu M. May 02 '14 at 17:30
  • I forgot to mention than thanks to Jonathan I had thoroughly changed my answer to include a layman's interpretation of what's going on in the compiler; hopefully this should make it much clearer what's going on here and thus help shape out future answers to avoid the same woes. – Matthieu M. May 02 '14 at 18:52

5 Answers5

8

The C++ standard says, in [temp.class.spec.match] paragraph 2:

A partial specialization matches a given actual template argument list if the template arguments of the partial specialization can be deduced from the actual template argument list (14.8.2).

14.8.2 is [temp.arg.deduct] i.e. the clause describing template argument deduction for function templates.

If you modify your code to use a similar function template and attempt to call it, you will see that the arguments cannot be deduced:

template <typename String, typename T>
void deduction_test(String,
                    typename basic_data_object<String, std::allocator>::template array_container<T>)
{ }

int main()
{
  deduction_test(std::string{}, std::vector<int, std::allocator<int>>{});
}

(I removed the Allocator parameter, since there's no way to pass a template template parameter as a function argument and in the basic_data_object type it's a non-deduced context, I don't believe it affects the result.)

Both clang and GCC say they cannot deduce T here. Therefore the partial specialization will not match the same types used as template arguments.

So I haven't really answered the question yet, only clarified that the reason is in the rules of template argument deduction, and shown an equivalence with deduction in function templates.

In 14.8.2.5 [temp.deduct.type] we get a list of non-deduced contexts that prevent deduction, and the following rule in paragraph 6:

When a type name is specified in a way that includes a non-deduced context, all of the types that comprise that type name are also non-deduced.

Since basic_data_object<String, Allocator> is in a non-deduced context (it is a nested-name-specifier, i.e. appears before ::) that means the type T is also non-deduced, which is exactly what Clang and GCC tell us.


With your temporary hardcoded typedef there is no non-deduced context, and so deduction for T succeeds using the deduction_test function template:

template <typename String, typename T>
void deduction_test(String,
                    typename data_object::template array_container<T>)
{ }

int main()
{
  deduction_test(std::string{}, std::vector<int, std::allocator<int>>{}); // OK
}

And so, correspondingly, your class template partial specialization can be matched when it uses that type.


I don't see a way to make it work without changing the definition of get_data_object_value, but if that's an option you can remove the need to deduce the array_container type and instead use a trait to detect whether a type is the type you want, and specialize on the result of the trait:

#include <string>
#include <vector>
#include <iostream>

template <typename String, template<class> class Allocator>
class basic_data_object
{
public:
  template<typename T>
  using array_container = std::vector<T, Allocator<T>>;

  template<typename T>
    struct is_ac : std::false_type { };

  template<typename T>
    struct is_ac<array_container<T>> : std::true_type { };
};

template <typename String, template<class> class Allocator, typename T, bool = basic_data_object<String, Allocator>::template is_ac<T>::value>
struct get_data_object_value
{
};

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value<String, Allocator, T, true>
{
  void f() { }
};

int main()
{
  get_data_object_value<std::string,std::allocator,std::vector<short>> obj;
  obj.f();
}

This doesn't really scale if you wanted several class template partial specializations, as you would need to add several bool template parameters with default arguments.

Jonathan Wakely
  • 153,269
  • 21
  • 303
  • 482
  • 1
    Function template specialization works differently than class template specialization. – Danvil May 02 '14 at 14:29
  • @Danvil, my answer does not mention function template specialization, because I'm not talking about function template specialization. However, class template partial specialization relies on template argument deduction, which works the same as for function templates. The standard says so. – Jonathan Wakely May 02 '14 at 14:34
  • Yes, but as you can see, deduction for classes is different than for functions. – Danvil May 02 '14 at 14:57
  • No, I can't see that. Where am I meant to see that? The quote I gave from the standard even has a reference from [temp.class.spec.match] to [temp.arg.deduct]. Can you quote from the standard to support your claim (N.B. there is no "deduction for classes", I chose my words carefully when I said "class template partial specialization _relies_ on template argument deduction") – Jonathan Wakely May 02 '14 at 14:59
  • There are a lot of specific rules in [temp.class.spec] (14.5.5) which apply only to classes. Does that not mean that it works differently? – Danvil May 02 '14 at 15:02
  • The only part of 14.5.5 that mentions "deduction" is 14.5.5.1, which is where I got the quote in my answer, which refers to 14.8.2, so I don't know what you're talking about, sorry. In any case, my answer shows a direct correspondence between matching partial specializations and argument deduction for function templates, when one fails, the other fails, and when one succeeds the other succeeds. Can you prove otherwise? – Jonathan Wakely May 02 '14 at 15:06
  • If you change the definition of `array_container` to `using array_container = std::vector;` (no longer a template, so some adaptations are necessary) then the OP code works and the specialization is elected. Thus I challenge the *non-deduced context* claim made here -- *You can check 3 simplified code examples in my [answer](http://stackoverflow.com/a/23431650/147192).* – Matthieu M. May 02 '14 at 15:25
  • You brilliantly explained what is happening thus answering the main first part of the question, a viable workaround is missing though :). I'll therefore wait for the bounty to end so you will earn half of it. – Drax May 09 '14 at 08:41
6

For some reason, the problem seems to stem from the double level of templates. I'll leave you check the 3 test cases below, they are simple:

  1. Remove the template arguments of First: works as expected
  2. Make First a template, but the inner type a plain one: works as expected
  3. Make both First and the inner type templates: compiles but the output is unexpected

Note: the template template parameter Allocator is useless to reproduce the issue, so I left it out.

Note: both GCC (ideone's version, 4.8.1 I believe) and Clang (Coliru version, 3.4) compile the code, and yet produce the same baffling result

From the 3 above examples, I deduce:

  • that this is NOT a non-deducible context issue; otherwise why would (2) work ?
  • that this is NOT an alias issue; otherwise why would (1) work ?

And therefore that either the problem is much hairier than the current hints would make us believe OR that both gcc and Clang have a bug.

EDIT: Thanks to Jonathan Wakely who patiently educated me enough that I could finally understand both the Standard wording related to this case and how it applied. I will now attempt to explain this (again) in my own words. Please refer to Jonathan's answer for the exact Standard quotes (it all sits in [temp.deduct.type])

  • When deducing template parameters (Pi), whether for functions or classes, the deduction is done independently for each and every argument.
  • Each argument need provide zero or one candidate Ci for each parameter; if an argument would provide more than one candidate, it provides none instead.
  • Thus, each argument produces a dictionary Dn: Pi -> Ci which maps a subset (possibly empty) of the template parameters to be deduced to their candidate.
  • The dictionaries Dn are merged together, parameter by parameter:
    • if only one dictionary has a candidate for a given parameter, then this parameter is accepted, with this candidate
    • if several dictionaries have the same candidate for a given parameter, then this parameter is accepted, with this candidate
    • if several dictionaries have different incompatible (*) candidates for a given parameter, then this parameter is rejected
  • If the final dictionary is complete (maps each and every parameter to an accepted candidate) then deduction succeeds, otherwise it fails

(*) there seems to be a possibility for finding a "common type" from the available candidates... it is of no consequence here though.

Now we can apply this to the previous examples:

1) A single template parameter T exists:

  • pattern matching std::vector<int> against typename First::template ArrayType<T> (which is std::vector<T>), we get D0: { T -> int }
  • merging the only dictionary yields { T -> int }, thus T is deduced to be int

2) A single template parameter String exists

  • pattern matching std::vector<int> against String, we get D0: { String -> std::vector<int> }
  • pattern matching std::vector<int> against typename First<String>::ArrayType we hit a non-deducible context (many values of String could fit), we get D1: {}
  • merging the two dictionaries yields { String -> std::vector<int> }, thus String is deduced to be std::vector<int>

3) Two template parameters String and T exist

  • pattern matching std::vector<char> against String, we get D0: { String -> std::vector<char> }
  • pattern matching std::vector<int> against typename First<String>::template ArrayType<T> we hit a non-deducible context, we get D1: {}
  • merging the two dictionaries yields { String -> std::vector<char> }, which is an incomplete dictionary (T is absent) deduction fails

I must admit I had not considered yet that the arguments were resolved independently from one another, and therefore than in this last case, when computing D1 the compiler could not take advantage of the fact that D0 had already deduced a value for String. Why it is done in this fashion, however, is probably a full question of its own.


Without the outer template, it works, as in it prints "Specialized":

#include <iostream>
#include <vector>

struct First {
    template <typename T>
    using ArrayType = std::vector<T>;
};

template <typename T>
struct Second {
    void go() { std::cout << "General\n"; }
};

template <typename T>
struct Second < typename First::template ArrayType<T> > {
    void go() { std::cout << "Specialized\n"; }
};

int main() {
    Second < std::vector<int> > second;
    second.go();
    return 0;
}

Without the inner template, it works, as in it prints "Specialized":

#include <iostream>
#include <vector>

template <typename String>
struct First {
    using ArrayType = std::vector<int>;
};

template <typename String, typename T>
struct Second {
    void go() { std::cout << "General\n"; }
};

template <typename String>
struct Second < String, typename First<String>::ArrayType > {
    void go() { std::cout << "Specialized\n"; }
};


int main() {
    Second < std::vector<int>, std::vector<int> > second;
    second.go();
    return 0;
}

With both, it fails, as in it prints "General":

#include <iostream>
#include <vector>

template <typename String>
struct First {
    template <typename T>
    using ArrayType = std::vector<T>;
};

template <typename String, typename T>
struct Second {
    void go() { std::cout << "General\n"; }
};

template <typename String, typename T>
struct Second < String, typename First<String>::template ArrayType<T> > {
    void go() { std::cout << "Specialized\n"; }
};

int main() {
    Second < std::vector<char>, std::vector<int> > second;
    second.go();
    return 0;
}
Matthieu M.
  • 251,718
  • 39
  • 369
  • 642
  • 1
    Nice examples. It is to do with non-deduced contexts though, see my answer. The two cases that work do not have to deduce a type from a type name that includes a non-deduced context, as per 14.8.2.5 [temp.deduct.type]/6. In your first example `First` does not need to be deduced. In the second example `First` is a non-deduced context, but `ArrayType` does not need to be deduced. In the third example `First` is a non-deduced context and `T` needs to be deduced, which fails. – Jonathan Wakely May 02 '14 at 15:36
  • @JonathanWakely: I think I am *starting* to understand the Standard phrasing. So, in essence, the problem seems to be that, *naively*, I would expect `First` to be deduced once `String` is (ie, I would expect a *serial* resolution) however the Standard specifies instead that each argument should be resolved independently (in parallel) and only afterward the deductions' consistency should be checked (because a template parameter can only refer to one type/value/template class). I suppose there is a good reason for this, but I had not expected it! – Matthieu M. May 02 '14 at 16:07
  • @JonathanWakely: Still, if this is truly the case, I do not understand why case (2) works. If the arguments are truly resolved independently, then how could `typename First::ArrayType` be correctly deduced ? Or is it that precisely because any value of `String` would yield the right type (barring specializations), then it is compatible with the value of `String` deduced from the first argument when the time comes for unifying ? – Matthieu M. May 02 '14 at 16:15
  • @MatthieM. The compiler only has to deduce template arguments for each template parameter, not every type in the template argument list. For case (1) it has to deduce `T`, so if `Second>` is instantiated then it can deduce `T` as `X`. In case (2) it only has to deduce `String`, because that is the only template parameter, so if `Second` is instantiated it deduces `String` as `X`, and then substitutes it into `typename First::ArrayType` which forms a valid type, and then if `Y` is `First::ArrayType` the specialization matches and is used. – Jonathan Wakely May 02 '14 at 16:32
  • In (3) it has to deduce _both_ `String` and `T`, but as you say, deduction of each argument happens independently, not left-to-right. In the type name `First::template ArrayType` there is a parameter in a non-deduced context, so no types can be deduced from it at all (if I am reading the standard correctly, which is not guaranteed!) `String` can be deduced from the first argument, but that still leaves `T` undeduced, so deduction fails. – Jonathan Wakely May 02 '14 at 16:36
  • @JonathanWakely: Ah! That clinches. I had lost sight of what we were trying to deduce here. Thank you very much for your thorough follow-up. – Matthieu M. May 02 '14 at 16:49
  • This is very well summed up and a simple yet detailed overview of what is going on, thanks :) – Drax May 09 '14 at 14:04
  • @Drax: Thanks to you as well, it was a pretty useful exercise for me and I learned something along the process. – Matthieu M. May 09 '14 at 14:12
5

The answer of Jonathan Wakely gives the reason why your code does not work.

My answer shows you how to solve the problem.


In your example, the container type over which you want to specialize is defined outside of basic_data_object thus you can of course use it directly in your specialization:

template <typename S, template<class> class A, typename T>
struct get_data_object_value<S,A,std::vector<T,A>>
{ };

This definitely conforms with the standard and works with all compilers.


In the case where the type is defined in basic_data_object, you can move it out of the class.

Example: Instead of

template<typename S, template<class> class A>
struct a_data_object
{
    template<typename T>
    struct a_container
    { };
};

write this:

template<typename S, template<class> class A, typename T>
// you can perhaps drop S and A if not needed...
struct a_container
{ };

template<typename S, template<class> class A, typename T>
struct a_data_object
{
    // use a_container<S,A,T>
};

Now you can specialize with:

template <typename S, template<class> class A, typename T>
struct get_data_object_value<S,A,a_container<S,A,T>>
{ };

Note: The next "solution" is apparently a bug with GCC 4.8.1.

If the container is only defined in an enclosing template and can not be moved out you can do this:

  1. Get the container type out of basic_data_object:

    template<typename S, template<class> class A, typename T>
    using bdo_container = basic_data_object<S,A>::array_container<T>;
    
  2. Write a specialization for this type:

    template <typename S, template<class> class A, typename T>
    struct get_data_object_value<S,A,bdo_container<S,A,T>>
    { };
    
Danvil
  • 20,344
  • 18
  • 61
  • 85
  • `basic_data_object::array_container` should be `typename basic_data_object::template array_container`, shouldn't it? – Constructor May 02 '14 at 15:56
  • @Constructor: Oh well, now it's back to digging in the standard I suppose. – Danvil May 02 '14 at 16:01
  • 1
    Interesting work-around; unfortunately just *aliasing* will not get you out of this because aliases are just sugar-coating here, the deduction/resolution still occur on the underlying types. – Matthieu M. May 02 '14 at 16:11
  • By the way your previous code does not compile with clang 3.4 at [coliru.stacked-crooked.com](http://coliru.stacked-crooked.com/a/40efb24a0e708c6c). – Constructor May 02 '14 at 16:35
  • @Constructor: GCC seems to be confused about when template and typedef is required... – Danvil May 02 '14 at 20:57
  • I guess you meant `a_container`instead of `a_data_object` in the 3rd code sample of the second part ? – Drax May 09 '14 at 10:12
  • @Drax: Thanks for noting the mistake! I fixed it. – Danvil May 09 '14 at 13:42
2

Alternatively what is the best (smallest / cleanest) way to make this work?

Arguably, it is:

  • Write a SFINAE trait template Tr<String,Allocator,T> that determines whether T is the same as basic_data_object<String,Allocator>::array_container<T::E> for some type E - if such there be - that is T::value_type.
  • Provide template get_data_object_value with a 4th parameter defaulting to Tr<String,Allocator,T>::value
  • Write partial specializations of get_data_object_value instantiating that 4th parameter as true, false respectively.

Here is a demo program:

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

template <typename String, template<class> class Allocator>
struct basic_data_object
{
    template<typename T>
    using array_container = std::vector<T, Allocator<T>>;
};

template<typename T, typename String, template<class> class Allocator>
struct is_basic_data_object_array_container
/* 
    A trait template that has a `static const bool` member `value` equal to
    `true` if and only if parameter type `T` is a container type
    with `value_type E` s.t. 
        `T` = `basic_data_object<String,Allocator>::array_container<T::E>`
*/ 
{
    template<typename A> 
    static constexpr bool
    test(std::is_same<
            A,
            typename basic_data_object<String,Allocator>::template
                array_container<typename A::value_type>
            > *) {
        return std::is_same<
            A,
            typename basic_data_object<String,Allocator>::template
                array_container<typename A::value_type>
            >::value;
    }
    template<typename A> 
    static constexpr bool test(...) {
        return false;
    }
    static const bool value = test<T>(nullptr);
};


template <
    typename String, 
    template<class> class Allocator,
    typename T,
    bool Select = 
        is_basic_data_object_array_container<T,String,Allocator>::value
>           
struct get_data_object_value;


template <
    typename String, 
    template<class> class Allocator,
    typename T
>           
struct get_data_object_value<
    String,
    Allocator,
    T,
    false
>
{
    static void demo() {
        std::cout << "Is NOT a basic_data_object array_container" << std::endl;
    }
};

template <
    typename String, 
    template<class> class Allocator,
    typename T>
struct get_data_object_value<
    String, 
    Allocator,
    T,
    true
>
{
    static void demo() {
        std::cout << "Is a basic_data_object array_container" << std::endl;
    }
};

#include <list>
#include <memory>

using namespace std;

int main(int argc, char **argv)
{
    get_data_object_value<string,allocator,std::vector<short>>::demo();
    get_data_object_value<string,allocator,std::list<short>>::demo();
    get_data_object_value<string,allocator,short>::demo();
    return 0;
}

Built with gcc 4.8.2, clang 3.4. Output:

Is a basic_data_object array_container
Is NOT a basic_data_object array_container
Is NOT a basic_data_object array_container

VC++ 2013 will not compile this for lack of constexpr support. To accommodate that compiler the following less natural implementation of the trait may be used:

template<typename T, typename String, template<class> class Allocator>
struct is_basic_data_object_array_container
{
    template<typename A>
    static
    auto test(
        std::is_same<
            A,
            typename basic_data_object<String, Allocator>::template
                array_container<typename A::value_type>
            > *
        ) ->
            std::integral_constant<
                bool,
                std::is_same<
                    A,
                    typename basic_data_object<String, Allocator>::template
                        array_container<typename A::value_type>
                >::value
            >{}
    template<typename A>
    static std::false_type test(...);
    using type = decltype(test<T>(nullptr));
    static const bool value = type::value;
};
Mike Kinghan
  • 46,463
  • 8
  • 124
  • 156
1

Edit: This answer only works because of a bug in GCC 4.8.1


Your code works as expected if you drop the keyword template in your specialization:

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value
{
    void foo() { std::cout << "general" << std::endl; }
};

template <typename String, template<class> class Allocator, typename T>
struct get_data_object_value
<String, Allocator,
typename basic_data_object<String, Allocator>::array_container<T>>
//                                         ^^^^^^ no template!
{
    void foo() { std::cout << "special" << std::endl; }
};

Example tested with GCC 4.8.1:

int main()  {
    get_data_object_value<std::string,std::allocator,std::vector<int>> obj;
    obj.foo(); // prints "special"
}
Danvil
  • 20,344
  • 18
  • 61
  • 85