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