4

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++?

Arjonais
  • 563
  • 2
  • 17

1 Answers1

5

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:

  • You could extend the minimalistic unclean version with a static_assert to make sure that you are using the correct value type (Godbolt). This should work for all kinds of allocators as well as the std::array but it is not very clean.

      template<typename Cont>
      void f (Cont const& cont) {
          using T = Cont::value_type;
          static_assert(std::is_same<T,int>::value, "Container value type must be of type 'int'");
          return;
      }
    
  • You could add the std::allocator<T> as a default template argument which has the disadvantage that your template then won't work if somebody uses a container with custom allocator and will neither work with std::array (Godbolt):

      template<template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
      void f(Cont<int> const& cont) {
          return;
      }
    
  • Similar to your code you could specify the allocator as second template argument yourself. Again this won't work with another type of allocator (Godbolt):

      template<template <typename... Elem> class Cont>
      void f(Cont<int, std::allocator<int>> const& cont) {
          return;
      }
    
  • So probably the cleanest approach prior to C++20 would be to use SFINAE to SFINAE out (meaning you add a certain structure inside the template which makes the compilation file if it does not meet your requirements) all other implementations that are not using the data type int with type_traits (std::is_same from #include <type_traits>, Godbolt)

      template<typename T, typename Alloc,  
               template <typename T,typename Alloc> class Cont,
               typename std::enable_if<std::is_same<T,int>::value>::type* = nullptr>
      void f(Cont<T,Alloc> const& cont) {
          return;
      }
    

    or which are not integer types (std::is_integral, Godbolt) as this is much more flexible regarding the template parameter Alloc:

      template<typename T, typename Alloc, 
               template <typename T,typename Alloc> class Cont,
               typename std::enable_if<std::is_integral<int>::value>::type* = nullptr>
      void f(Cont<T,Alloc> const& cont) {
          return;
      }
    

    Furthermore this can be extended easily with logical or || and logical and &&. Since C++14 one might also the corresponding aliases and write std::enable_if_t<std::is_same_v<T,int>> instead of std::enable_if<std::is_same<T,int>::value>::type which makes it a little less awkward to read.

  • Finally in the newest standard C++20 you should even be able use the long-awaited concepts (#include <concepts>) using the Container concept (see also this Stackoverflow post) e.g. as follows (Wandbox)

      template<template <typename> typename Cont>
      requires Container<Cont<int>>
      void f(Cont<int> const& cont) {
          return;
      }
    
  • And similar in C++20 there exists std::span<T> which unlike all solutions above works with std::array as well (Wandbox)

      void f(std::span<int> const& cont) {
          return;
      }
    
2b-t
  • 2,414
  • 1
  • 10
  • 18
  • So, it is a bug in GCC and Clang for accepting that code? Is it my code or MSVC which are wrong? Even thought this answer is detailed, I cannot see how it answers my question. Also, I need the parameter to be fixed, and not a template parameter. – Arjonais Apr 20 '21 at 10:27
  • I have checked the reference you provided by Vandevoorde and I could not find relation to this problem. P.85: Debugging templates / afternotes / summary. P.197: Explicit specialization. Nothing about template templates in these pages. – Arjonais Apr 20 '21 at 10:30
  • Hey @Arjonais, as I said it depends on the C++ standard: For C++17 all of the above versions should work (so MSVC seems to be wrong in this regard), with C++11 the first two version should work (and they do for all compilers I have tested). As pointed out on bottom of page 85 "Template Template Argument Matching" in the second edition of this book (5.7) the latter version should theoretically work according to the standard but it does not seem to work with MSVC and Clang. I will update this update the post including a reference to the corresponding C++ standard in a couple of minutes. – 2b-t Apr 20 '21 at 11:11
  • I see... So what you are trying to achieve is a generic container `Cont`, which has always a data type `int` but the container might be different, right? You could always SFINAE out other template types. I will update my answer to include this as well. – 2b-t Apr 20 '21 at 11:13
  • @Arjonais Updated it and added a few more Godbolt example how your code should do what you want it to but yet compile on all said compilers! Let me know if there are still open points. – 2b-t Apr 20 '21 at 12:56
  • Thank you for your detailed answer. So, what I understand is that MSVC should accept the snippet of the question, but it does not. However, you also say that I have a small mistake in my code, but you do not mention where, despite the wide explanations. I'm still a bit unclear. Is a bug report to MSVC appropriate? Why do GCC and Clang do not report the mistake? – Arjonais Apr 21 '21 at 14:33
  • As I said, this answer might be too verbose. There is a discussion about C++11, C++17 and C++20. Also SFINAE, which I think is not relevant to the question. So the answer can be improved by making it more concise. In the first mention of SFINAE (https://godbolt.org/z/dTMebMnd9) it looks like it will accept any type, and SFINAE will not trigger because it only does so in the declaration of the function. – Arjonais Apr 21 '21 at 14:40
  • From my understanding of the standard MSVC should accept your code (like GCC, Clang and ICC) as it should apply the default argument `std::allocator`. So I guess you can bug-report it if you want to. The small mistake I mentioned was only a misunderstanding when I formulated the initial answer and I just forgot to remove the corresponding section. And you are right and the first snippet where SFINAE is mentioned actually does not use SFINAE! – 2b-t Apr 21 '21 at 22:22
  • And I agree, my answer is indeed verbose. I got a bit carried away - sorry for that! I guess the first five lines are the already the answer to your question. The next section just explains how template template matching works and that the reason for this bug is potentially the change in the standard from C++14 to C++17: Your code would be wrong in the C++14 standard (and e.g. the ICC 19.0 will fail compiling it!). The last paragraph tries to point out possible solutions to your problem that still compile on all said compilers seeing that your version is not accepted by MSVC. – 2b-t Apr 21 '21 at 22:39