【问题标题】:Class template specialization priority/ambiguity类模板专业化优先级/歧义
【发布时间】:2017-04-14 23:50:53
【问题描述】:

在尝试依赖可变参数模板实现一些事情时,我偶然发现了一些我无法解释的东西。我将问题归结为以下代码sn-p:

template <typename ... Args>
struct A {};

template <template <typename...> class Z, typename T>
struct test;

template <template <typename...> class Z, typename T>
struct test<Z, Z<T>> {
    static void foo() {
        std::cout << "I'm more specialized than the variadic spec, hehe!" << std::endl;
    }
};

template <template <typename...> class Z, typename T, typename ... Args>
struct test<Z, Z<T, Args...>> {
    static void foo() {
        std::cout << "I'm variadic!" << std::endl;
    }
};

int main() {
    test<A, A<int>>::foo();
}

在 gcc 下,它会产生错误,因为它在尝试实例化 test&lt;A, A&lt;int&gt;&gt; 时认为两种特化是同等特化的:

main.cpp: In function 'int main()':

main.cpp:25:24: error: ambiguous template instantiation for 'struct test<A, A<int> >'

         test<A, A<int>>::foo();

                        ^~

main.cpp:11:12: note: candidates are: template<template<class ...> class Z, class T> struct test<Z, Z<T> > [with Z = A; T = int]

     struct test<Z, Z<T>> {

            ^~~~~~~~~~~~~

main.cpp:18:12: note:                 template<template<class ...> class Z, class T, class ... Args> struct test<Z, Z<T, Args ...> > [with Z = A; T = int; Args = {}]

     struct test<Z, Z<T, Args...>> {

但是,clang 认为第一个专业化“更专业化”(通过部分排序:请参阅下一节),因为它编译良好并打印:

我比可变参数更专业,呵呵!

live demo 可以在 Coliru 上找到。我也尝试使用 gcc 的 HEAD 版本,得到了同样的错误。

我的问题是:由于这两个著名的编译器的行为不同,哪一个是正确的,这段代码是正确的 C++ 吗?


标准解释 (C++14 current draft)

根据 C++14 标准草案的 §14.5.5.1 和 $14.5.5.2 部分,会触发部分排序以确定应选择哪个专业化:

(1.2) — 如果找到多个匹配的特化,则使用偏序规则 (14.5.5.2) 来确定 其中一个专业是否比其他专业更专业。如果没有专业 比所有其他匹配的特化更特化,那么类模板的使用是 模棱两可,程序格式不正确。

现在根据 §14.5.5.2,类模板特化通过这个过程转换为函数模板:

对于两个类模板部分特化,如果,第一个比第二个更特化,给定 重写为两个函数模板后,第一个函数模板比​​第二个更专业 根据功能模板的排序规则(14.5.6.2):

(1.1) — 第一个函数模板与第一个部分特化具有相同的模板参数,并且具有 单个函数参数,其类型是具有模板参数的类模板特化 第一部分专业化,和

(1.2) — 第二个函数模板与第二个偏特化具有相同的模板参数 并且有一个函数参数,其类型是模板的类模板特化 第二部分特化的参数。

因此,我尝试使用上述转换应生成的函数模板重载来重现问题:

template <typename T>
void foo(T const&) {
    std::cout << "Generic template\n";
}

template <template <typename ...> class Z, typename T>
void foo(Z<T> const&) {
    std::cout << "Z<T>: most specialized overload for foo\n";
}

template <template <typename ...> class Z, typename T, typename ... Args>
void foo(Z<T, Args...> const&) {
    std::cout << "Z<T, Args...>: variadic overload\n";
}

现在尝试像这样使用它:

template <typename ... Args>
struct A {};

int main() {
    A<int> a;
    foo(a);
}

在 clang 和 gcc 中都会产生编译错误 [模糊调用]:live demo。我预计 clang 至少会具有与类模板案例一致的行为。

然后,这是我对标准的解释(我似乎与@Danh 分享),所以此时我们需要 来确认这一点。

注意:我浏览了一点 LLVM 的错误跟踪器,但找不到在这个问题中观察到的函数模板重载行为的票证。

【问题讨论】:

  • 可变参数模板参数不是指0 to N参数吗?在那种情况下,g++ 似乎是正确的。
  • @Arunmu 模板本身不是可变参数。我依赖可变参数模板来完成其中一项专业,通常完全是fine
  • 不管它是什么,它都不是偏序——它只适用于函数模板,不适用于类模板。
  • @Rerito 我说Args... 很可能是一个空集,这使得你的两个专业化对于单个模板参数都是相同的,至少我的猜测是这样。
  • 通过删除Z,gcc/clang 同意更专业的Demo。所以,我会说 gcc 的错误。

标签: language-lawyer c++ c++11 language-lawyer variadic-templates template-specialization


【解决方案1】:

来自temp.class.order

对于两个类模板部分特化,如果根据函数模板的排序规则,对两个函数模板进行以下重写,第一个函数模板比​​第二个函数模板更特化 (@987654322) @):

  • 两个函数模板中的每一个都具有与对应的偏特化相同的模板参数。

  • 每个函数模板都有一个函数参数,其类型是类模板特化,其中模板参数是来自函数模板的对应模板参数,用于模板参数列表的简单模板 ID 的模板参数列表部分专业化。

顺序:

template <template <typename...> class Z, typename T>
struct test<Z, Z<T>> {
    static void foo() {
        std::cout << "I'm more specialized than the variadic spec, hehe!" << std::endl;
    }
};

template <template <typename...> class Z, typename T, typename ... Args>
struct test<Z, Z<T, Args...>> {
    static void foo() {
        std::cout << "I'm variadic!" << std::endl;
    }
};

取决于以下顺序:

template <template <typename...> class Z, typename T>
void bar(test<Z, Z<T>>); // #1
template <template <typename...> class Z, typename T, typename ... Args>
void bar(test<Z, Z<T, Args...>>); // #2

来自[temp.func.order]

部分排序通过依次转换每个模板(参见下一段)并使用函数类型执行模板参数推导来选择两个函数模板中的哪一个比另一个更专业。推演过程确定模板中的一个是否比另一个更专业。如果是这样,更专业的模板是偏排序过程选择的模板。

为了生成转换后的模板,对于每个类型、非类型或模板模板参数(包括其模板参数包([temp.variadic]))分别合成一个唯一的类型、值或类模板并将其替换为该参数在模板的函数类型中的每次出现。

使用转换后的函数模板的函数类型,对另一个模板执行类型推导,如[temp.deduct.partial] 中所述。

通过这些段落,对于从任何合成模板Z0 和类型T0 转换的任何函数,可以形成#1,我们可以用#2 进行类型推导。但是,从#2 与任何类型T2Args2 的任何非空集的虚构模板Z2 转换的函数不能从#1 推导出来。 #1 显然比 #2 更专业。

clang++ 在这种情况下是正确的。


实际上,this onethis one 在 g++ 和 clang 中都无法编译(因为模棱两可)。似乎两个编译器都很难使用模板模板参数。 (后一个是明确排序的,因为它的顺序与没有函数调用的顺序相同)。

【讨论】:

  • 有趣。第二个“this one”确实使用 Microsoft v140 成功编译。
  • 我对标准有相同的解释(我昨天看了看确认了这一点)。但是,我尝试使用函数模板来重现该行为,结果在 clang 和 gcc 中都失败了,just like you
  • @Rerito 第一次解释标准时,我认为 g++ 是对的(你可以在帖子历史中看到),因为 g++ 和 clang 都无法编译函数模板。在编译另一个简单案例(我的最后一个例子)时,我花了很多时间才意识到他们两个都错了
  • @Danh 完全正确。这很麻烦,因为当将部分类模板特化转换为函数模板重载时,clang 应该以我们实现的相同函数模板结束......但是它无法推断出函数模板的良好重载?!这也可以使用没有模板模板参数的 clang 重现:see this。但在最后一个例子中,g++ 做对了……
猜你喜欢
  • 2020-07-18
  • 1970-01-01
  • 1970-01-01
  • 2016-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-24
  • 1970-01-01
相关资源
最近更新 更多