【问题标题】:Visual C++ cannot deduce template template parameterVisual C++ 无法推导出模板模板参数
【发布时间】:2021-07-12 07:59:51
【问题描述】:

以下 C++17 代码的 sn-p 在 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++ 中的解释不同?

【问题讨论】:

    标签: c++ templates c++17 template-argument-deduction template-templates


    【解决方案1】:

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

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


    std::容器的模板参数

    正如帖子中指出的那样,您已经链接了标准库容器,例如 std::vectorstd::dequestd::list 实际上有多个模板参数。第二个参数Alloc 是描述内存分配的策略特征,具有默认值std::allocator&lt;T&gt;

    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)

    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&lt;T&gt;未考虑(请参阅“模板模板参数”部分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-expression 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 中使用 可变参数模板 实现相同的功能(因此数据类型是模板参数包 T 的第一个模板参数,分配器是第二个模板参数)如下(Godbolt MSVC):

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

    现在在 C++17 中,实际上以下行应该编译并使用所有带有 std::allocator&lt;T&gt;std:: 容器(参见第 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 扩展简约的 unclean 版本,以确保您使用的是正确的值类型 (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&lt;T&gt; 添加为默认模板参数,其缺点是如果有人使用带有自定义分配器的容器,您的模板将无法工作,并且不会与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 将 SFINAE 输出(意味着您在模板中添加某个结构,如果它不满足您的要求则生成编译文件)所有其他实现没有使用数据类型inttype_traitsstd::is_same 来自#include &lt;type_traits&gt;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_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;
        }
      

      此外,这可以是extended easily with logical or || and logical and &amp;&amp;。由于 C++14 还可以使用相应的别名并写 std::enable_if_t&lt;std::is_same_v&lt;T,int&gt;&gt; 而不是 std::enable_if&lt;std::is_same&lt;T,int&gt;::value&gt;::type,这使得阅读起来不那么尴尬。

    • 终于在最新的标准 C++20 中,您甚至应该可以使用期待已久的 concepts (#include &lt;concepts&gt;) 使用 Container concept(另请参阅此 Stackoverflow post),例如如下(Wandbox

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

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

    【讨论】:

    • 那么,接受该代码是 GCC 和 Clang 中的一个错误吗?是我的代码还是 MSVC 错误?即使这个答案很详细,我也看不出它是如何回答我的问题的。另外,我需要固定参数 ,而不是模板参数。
    • 我检查了 Vandevoorde 提供的参考资料,但找不到与此问题相关的信息。 P.85:调试模板/后记/摘要。 P.197:显式专业化。这些页面中没有关于模板模板的内容。
    • 嘿@Arjonais,正如我所说,这取决于 C++ 标准:对于 C++17,所有上述版本都应该工作(所以 MSVC 在这方面似乎是错误的),使用 C++ 11 前两个版本应该可以工作(它们适用于我测试过的所有编译器)。正如本书第二版(5.7)中第 85 页“模板模板参数匹配”底部所指出的那样,后一个版本理论上应该按照标准工作,但它似乎不适用于 MSVC 和 Clang。我将在几分钟内更新此更新帖子,包括对相应 C++ 标准的引用。
    • 我明白了......所以你想要实现的是一个通用容器Cont&lt;int&gt;,它总是有一个数据类型int,但容器可能不同,对吧?您总是可以 SFINAE 出其他模板类型。我将更新我的答案以包括这一点。
    • @Arjonais 更新了它并添加了更多 Godbolt 示例,您的代码应该如何执行您想要的操作,但在所有上述编译器上编译!让我知道是否还有开放点。
    猜你喜欢
    • 1970-01-01
    • 2020-07-09
    • 2021-06-16
    • 2010-12-22
    • 1970-01-01
    • 1970-01-01
    • 2019-11-29
    • 2023-03-09
    • 1970-01-01
    相关资源
    最近更新 更多