c++templatesc++17template-argument-deductiontemplate-templates

Visual C++ cannot deduce template template parameter


The following snippet of C++17 code compiles in GCC and CLang, but in Visual C++ it gives these errors:

<source>(14): error C2672: 'f': no matching overloaded function found
<source>(14): error C2784: 'std::ostream &f(std::ostream &,const container<int> &)': could not deduce template argument for 'const container<int> &' from 'const std::vector<int,std::allocator<int>>'
<source>(5): note: see declaration of 'f'

https://godbolt.org/z/aY769qsfK

#include <vector>

template< template <typename...> typename container >
void f (const container< int > &)
{ }

int main()
{
    std::vector<int> seq = {1, 2, 3};
    f<std::vector>(seq); // OK
    f(seq);              // ERROR
}

Note that this code is similar to one of the answers in Why compiler cannot deduce template template argument?

Is it a problem of the code? Or a problem in Visual C++? Maybe some ambiguity in the C++ standard that is interpreted different in GCC and Visual C++?


Solution

  • I have encountered this as well with Visual C++ and I think in this regard the Visual C++ compiler is not compliant with the C++17 standard and your code is correct (but your code won't work with an std::vector with custom allocator!). The standard containers have in fact two template parameters: The value type and the allocator (which defaults to std::allocator<T>). Prior to C++17 template template matching required the template parameters to match exactly while in C++17 this was relaxed to include default arguments as well. Yet for some reason Visual C++ seems still to expect the second template argument std::allocator<T> and not assume the given default argument.

    The following sections will discuss the template template matching for the different standards in more detail. At the end of the post I will suggest alternatives that will make your code compile on all said compilers which takes the form of SFINAE with two two template arguments (so that it works with custom allocators as well) for C++17 and std::span for C++20 and onwards. std::span actually does not need any template at all.


    Template parameters of std:: Containers

    As pointed out in the post that you linked already standard-library containers such as std::vector, std::deque and std::list actually have more than one template parameter. The second parameter Alloc is a policy trait which describes the memory allocation and has a default value std::allocator<T>.

    template<typename T, typename Alloc = std::allocator<T>>
    

    Contrary std::array actually uses two template parameters T for the data type and std::size_t N for the container size. This means if one wants to write a function that covers all said containers one would have to turn to iterators. Only in C++20 there is a class template for contiguous sequences of objects std::span (which is sort of a super-concept that encapsulates all of the above) that relaxes this.

    Template template matching and the C++ standard

    When writing a function template whose template arguments themselves depend on template parameters you will have to write a so called template template function, meaning a function of the form:

    template<template<typename> class T>
    

    Note that strictly according to the standard template template parameters would have to be of declared with class not with typename prior to C++17. You could certainly somehow circumvent such a template template construct (from C++11 onwards) with a very minimal solution such as (Godbolt)

    template<typename Cont>
    void f (Cont const& cont) {
        using T = Cont::value_type;
        return;
    }
    

    which assumes that the container contains a static member variable value_type which is then used to define the underlying data type of the elements. This will work for all said std:: containers (including the std::array!) but is not very clean.

    For template template function there exist particular rules which actually changed from C++14 to C++17: Prior to C++17 a template template argument had to be a template with parameters that exactly match the parameters of the template template parameter it substitutes. Default arguments such as the second template argument for the std:: containers, the aforementioned std::allocator<T>, were not considered (See the "Template template argument" section here as well as in the section "Template template arguments" on the page 317 of this working draft of the ISO norm or the final C++17 ISO norm):

    To match a template template argument A to a template template parameter P, each of the template parameters of A must match corresponding template parameters of P exactly (until C++17) P must be at least as specialized as A (since C++17).

    Formally, a template template-parameter P is at least as specialized as a template template argument A if, given the following rewrite to two function templates, the function template corresponding to P is at least as specialized as the function template corresponding to A according to the partial ordering rules for function templates. Given an invented class template X with the template parameter list of A (including default arguments):

    • Each of the two function templates has the same template parameters, respectively, as P or A.
    • Each function template has a single function parameter whose type is a specialization of X with template arguments corresponding to the template parameters from the respective function template where, for each template parameter PP in the template parameter list of the function template, a corresponding template argument AA is formed. If PP declares a parameter pack, then AA is the pack expansion PP...; otherwise, AA is the id-expression PP.

    If the rewrite produces an invalid type, then P is not at least as specialized as A.

    Therefore prior to C++17 one would have to write a template mentioning the allocator as a default value manually as follows. This works also in Visual C++ but as all the following solutions will exclude the std::array (Godbolt MSVC):

    template<typename T, 
             template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
    void f(Cont<T> const& cont) {
        return;
    }
    

    You could achieve the same thing in C++11 also with variadic templates (so that the data-type is the first and the allocator the second template argument of the template parameter pack T) as follows (Godbolt MSVC):

    template<template <typename... Elem> class Cont, typename... T>
    void f (Cont<T...> const& cont) {
        return;
    }
    

    Now in C++17 actually the following lines should compile and work with all std:: containers with the std::allocator<T> (See section 5.7 on pages 83-88, in particular "Template Template Matching" on page 85, of "C++ Templates: The complete guide (second edition)" by Vandevoorde et al., Godbolt GCC).

    template<typename T, template <typename Elem> typename Cont>
    void f (Cont<T> const& cont) {
        return;
    }
    

    The quest for a generic std:: container template

    Now if your goal is to use a generic container that only holds integers as template arguments and you have to guarantee that it compiles on Visual C++ as well then you have following options: