微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决Visual C++ 无法推导出模板模板参数

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

请注意,此代码类似于 Why compiler cannot deduce template template argument?

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

解决方法

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

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


std:: 容器的模板参数

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

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

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

模板模板匹配和 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 部分以及 page 317 of this working draft of the ISO normthe final C++17 ISO norm) 上的“模板模板参数”部分:

将模板模板参数 A 匹配到模板模板 参数P,A的每个模板参数必须匹配 P的对应模板参数正好(直到C++17) P 必须至少与 A 一样专业(C++17 起)

形式上,模板模板参数 P 至少是专门的 作为模板模板参数 A if,给定以下重写为 两个函数模板,P对应的函数模板在 与 A 对应的函数模板最不专业 根据函数模板的偏序规则。给定的 一个带有 A 的模板参数列表的发明类模板 X (包括默认参数):

  • 两个函数模板中的每一个都具有相同的模板参数,分别为 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;
}

您也可以使用可变参数模板在 C++11 中实现相同的功能(因此数据类型是模板参数包的第一个模板参数,而分配器是模板参数包的第二个模板参数 {{1 }}) 如下 (Godbolt MSVC):

T

现在在 C++17 中,以下几行实际上应该编译并与所有 template<template <typename... Elem> class Cont,typename... T> void f (Cont<T...> const& cont) { return; } 容器一起使用 std::(参见第 83-88 页的第 5.7 节,特别是“模板模板匹配”)第 85 页,共 "C++ Templates: The complete guide (second edition)" by Vandevoorde et al.Godbolt GCC)。

std::allocator<T>

寻求通用的 template<typename T,template <typename Elem> typename Cont> void f (Cont<T> const& cont) { return; } 容器模板

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

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

    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> 一起工作(Godbolt):

    std::array
  • 与您的代码类似,您可以自己指定分配器作为第二个模板参数。同样,这不适用于其他类型的分配器 (Godbolt):

      template<template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
      void f(Cont<int> const& cont) {
          return;
      }
    
  • 因此,在 C++20 之前,最简洁的方法可能是使用 SFINAE 将 SFINAE 输出(这意味着您在模板中添加了某个结构,如果它不符合您的要求,则会生成编译文件)要求)所有其他未将数据类型 template<template <typename... Elem> class Cont> void f(Cont<int,std::allocator<int>> const& cont) { return; } int 一起使用的实现(type_traits from std::is_same,Godbolt

    #include <type_traits>

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

    Alloc

    此外,这可以是 extended easily with logical or || and logical and &&。由于 C++14 还可以使用相应的别名并编写 template<typename T,typename std::enable_if<std::is_integral<int>::value>::type* = nullptr> void f(Cont<T,Alloc> const& cont) { return; } 而不是 std::enable_if_t<std::is_same_v<T,int>>,这使得阅读起来不那么尴尬。

  • 最后在最新的标准 C++20 中,您甚至应该能够使用期待已久的 concepts (#include <concepts>) 使用 Container concept(另请参阅此 {{ 3}}) 例如如下 (Stackoverflow post)

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

    std::array

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。