Visual C++ 无法推导模板模板参数

Visual C++ cannot deduce template template parameter

以下 C++17 代码片段可在 GCC 和 CLang 中编译,但在 Visual C++ 中会出现以下错误:

<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
}

请注意,此代码类似于

是不是代码的问题?还是 Visual C++ 中的问题?也许在 GCC 和 Visual C++ 中解释不同的 C++ 标准中存在一些歧义?

我在使用 Visual C++ 时也遇到过这个问题,我认为在这方面 Visual C++ 编译器不符合 C++17 标准,您的代码是正确的(但您的代码无法与带有自定义分配器的 std::vector 一起使用!)。标准容器实际上有两个模板参数:值类型和分配器(默认为 std::allocator<T>)。在 C++17 之前 template 模板匹配 要求模板参数完全匹配,而在 C++17 中这被放宽到包括 默认参数。然而出于某种原因,Visual C++ 似乎仍然期望第二个模板参数 std::allocator<T> 而不是假定给定的默认参数。

以下部分将更详细地讨论针对不同标准的模板模板匹配。在 post 的末尾,我将建议替代方案,这些替代方案将使您的代码在所有上述编译器上编译,这些编译器采用 SFINAE 的形式,带有两个两个模板参数(以便它工作以及自定义分配器)用于 C++17 和 std::span 用于 C++20 及以后版本。 std::span其实根本不需要任何模板。


std:: 个容器的模板参数

正如在 post 中指出的那样,您链接的标准库容器(例如 std::vector, std::deque and std::list 实际上具有 多个模板参数 。第二个参数 Alloc 是描述内存分配的策略特征,默认值为 std::allocator<T>.

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

相反 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 . Only in C++20 there is a class template for contiguous sequences of objects std::span(这是一种封装了上述所有内容的超级概念)放松了这一点。

Template模板匹配和C++标准

当编写其模板参数本身依赖于模板参数的函数模板时,您将必须编写所谓的模板模板函数,即以下形式的函数:

template<template<typename> class T>

请注意,严格按照标准模板,在 C++17 之前,模板参数必须使用 class 而不是 typename 进行声明。你当然可以用一个非常小的解决方案(Godbolt

来绕过这样的模板构造(从 C++11 开始)
template<typename Cont>
void f (Cont const& cont) {
    using T = Cont::value_type;
    return;
}

假定容器包含一个静态成员变量 value_type,然后用于定义元素的基础数据类型。这适用于所有提到的 std:: 容器(包括 std::array!),但不是很干净。

对于模板模板函数,存在特定的规则,这些规则实际上从 C++14 更改为 C++17:在 C++17 之前 模板模板参数必须是一个模板,其参数与它所替代的模板模板参数的参数完全匹配。 默认参数,例如 std:: 容器的第二个模板参数,即前面提到的 std::allocator<T>未考虑(请参阅“模板模板参数”部分 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.

因此,在 C++17 之前,必须编写一个模板,手动提及 分配器作为默认值,如下所示。这也适用于 Visual C++,但由于以下所有解决方案都将排除 std::array (Godbolt MSVC):

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

您也可以在 C++11 中使用 可变模板 实现同样的事情(这样数据类型是模板的第一个模板参数,分配器是模板的第二个模板参数参数包T)如下(Godbolt MSVC):

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

现在在 C++17 中,实际上以下行应该编译并与所有 std:: 容器一起使用 std::allocator<T>(请参阅第 83-88 页的第 5.7 节,特别是“模板模板匹配” ”第 85 页,共 "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;
}

寻求通用 std:: 容器模板

现在,如果您的目标是使用仅将整数作为模板参数的通用容器,并且您必须保证它也能在 Visual C++ 上编译,那么您有以下选项:

  • 您可以使用 static_assert 扩展简约的不干净版本,以确保您使用的是正确的值类型 (Godbolt)。这应该适用于所有类型的分配器以及 std::array 但它不是很干净。

      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;
      }
    
  • 您可以将 std::allocator<T> 添加为默认模板参数,其缺点是如果有人使用带有自定义分配器的容器并且您的模板将无法使用 std::array (Godbolt):

      template<template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
      void f(Cont<int> const& cont) {
          return;
      }
    
  • 与您的代码类似,您可以自己将分配器指定为第二个模板参数。同样,这不适用于另一种类型的分配器 (Godbolt):

      template<template <typename... Elem> class Cont>
      void f(Cont<int, std::allocator<int>> const& cont) {
          return;
      }
    
  • 所以在 C++20 之前最干净的方法可能是使用 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;
      }
    

    或者不是整数类型 (std::is_integral, Godbolt) 因为这对于模板参数 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;
      }
    

    此外这可以是。由于 C++14 也可能使用相应的别名并编写 std::enable_if_t<std::is_same_v<T,int>> 而不是 std::enable_if<std::is_same<T,int>::value>::type 这使得阅读起来不那么尴尬。

  • 终于在最新的标准中 C++20 你甚至应该可以使用期待已久的 concepts (#include <concepts>) using the Container concept (see also this ) e.g. as follows (Wandbox)

      template<template <typename> typename Cont>
      requires Container<Cont<int>>
      void f(Cont<int> const& cont) {
          return;
      }
    
  • 与 C++20 类似,存在 std::span<T> 与上述所有解决方案不同,它也适用于 std::arrayWandbox)

      void f(std::span<int> const& cont) {
          return;
      }