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

Arj*_*ais 4 c++ templates template-templates template-argument-deduction c++17

以下 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'
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

请注意,此代码类似于 为什么编译器无法推导模板模板参数?中的答案之一。

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

2b-*_*b-t 5

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

以下部分将更详细地讨论不同标准的模板匹配。在文章的最后,我将建议替代方案,使您的代码在所有上述编译器上进行编译,该编译器采用SFINAE的形式,带有两个两个模板参数(以便它也可以与自定义分配器一起使用),适用于 C++17 和std::spanC ++20 及以上。std::span 实际上根本不需要任何模板。


std::容器模板参数

正如文章中指出的,您已经链接了标准库容器,例如std::vectorstd::deque并且std::list实际上有多个模板参数。第二个参数Alloc是一个策略特征,它描述内存分配并具有默认值std::allocator<T>

template<typename T, typename Alloc = std::allocator<T>>
Run Code Online (Sandbox Code Playgroud)

相反,std::array实际上使用两个模板参数T来表示数据类型和std::size_t N容器大小。这意味着如果想要编写一个涵盖所有上述容器的函数,则必须求助于迭代器。仅在 C++20 中,有一个用于连续对象序列的类模板std::span(这是一种封装了上述所有内容的超级概念),可以放松这一点。

模板模板匹配和C++标准

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

template<template<typename> class T>
Run Code Online (Sandbox Code Playgroud)

请注意,严格按照标准模板,在 C++17 之前,模板参数必须使用classnot with声明。typename你当然可以用一个非常小的解决方案(例如(Godbolt) )以某种方式绕过这样的模板模板构造(从 C++11 开始)

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

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

对于模板模板函数,存在特定的规则,这些规则实际上从 C++14 更改为 C++17:在 C++17 之前,模板模板参数必须是一个模板,其参数与模板模板参数的参数完全匹配。替代品。未考虑默认参数,例如容器的第二个模板参数(std::前面提到的)(请参阅此处的“模板模板参数”部分以及ISO 本工作草案第 317 页上的“模板模板参数”部分)规范最终的 C++17 ISO 规范):std::allocator<T>

要将模板模板参数 A 与模板模板参数 P 匹配,A 的每个模板参数必须与 P 的对应模板参数完全匹配(直到 C++17), P 必须至少与 A 一样专业化(因为 C++ 17)

形式上,模板模板参数 P 至少与模板模板实参 A 一样专业化,如果给定以下对两个函数模板的重写,根据下式,对应于 P 的函数模板至少与对应于 A 的函数模板一样专业化:函数模板的部分排序规则。给定一个发明的类模板 X,其模板参数列表为 A(包括默认参数):

  • 两个函数模板均具有相同的模板参数,分别为 P 或 A。
  • 每个函数模板都有一个函数参数,其类型是 X 的特化,模板参数对应于相应函数模板中的模板参数,其中,对于函数模板的模板参数列表中的每个模板参数 PP,有一个对应的模板参数 AA形成了。如果PP声明了一个参数包,那么AA就是包扩展PP...;否则,AA 是 id-表达式 PP。

如果重写产生无效类型,则 P 至少不像 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

现在,在 C++17 中,实际上以下几行应该编译并与所有std::容器一起使用(请参阅“C++ 模板:完整指南”(第二个)第std::allocator<T>83-88 页上的第 5.7 节,特别是第 85 页上的“模板模板匹配”版)”,作者:Vandevoorde 等人Godbolt GCC)。

template<typename T, template <typename Elem> typename Cont>
void f (Cont<T> const& cont) {
    return;
}
Run Code Online (Sandbox Code Playgroud)

寻求通用std::容器模板

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

  • 您可以使用 a 扩展简约的不干净版本,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;
      }
    
    Run Code Online (Sandbox Code Playgroud)
  • 您可以添加std::allocator<T>作为默认模板参数,其缺点是如果有人使用带有自定义分配器的容器并且两者都无法使用std::arrayGodbolt),则您的模板将无法工作:

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

      template<template <typename... Elem> class Cont>
      void f(Cont<int, std::allocator<int>> const& cont) {
          return;
      }
    
    Run Code Online (Sandbox Code Playgroud)
  • 因此,C++20 之前最干净的方法可能是使用SFINAE来 SFINAE 输出(这意味着您在模板内添加某个结构,如果它不满足您的要求,则会生成编译文件)所有其他不使用数据int类型type_traits( std::is_samefrom #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;
      }
    
    Run Code Online (Sandbox Code Playgroud)

    或者不是整数类型(std::is_integralGodbolt ,因为这对于模板参数来说更加灵活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;
      }
    
    Run Code Online (Sandbox Code Playgroud)

    此外,这可以通过逻辑 or||和逻辑 and&&轻松扩展。从 C++14 开始,我们还可以使用相应的别名并写入std::enable_if_t<std::is_same_v<T,int>>,而不是这样std::enable_if<std::is_same<T,int>::value>::type使得阅读起来不那么尴尬。

  • 最后,在最新的标准C++20中,您甚至应该能够使用期待已久的概念 ( #include <concepts>),使用容器概念(另请参阅此Stackoverflow 帖子),例如如下 ( Wandbox )

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

      void f(std::span<int> const& cont) {
          return;
      }
    
    Run Code Online (Sandbox Code Playgroud)