函数模板具有未推导上下文的部分排序

25

在阅读另一个问题时,我遇到了部分排序的问题,我将其简化为以下测试用例

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

int main() {
  // GCC chokes on f(0, 0) (not being able to match against T1)
  void *p = 0;
  f(0, p);
}

对于这两个函数模板,进入重载决议的特化的函数类型为void(int,void *)。但是部分排序(根据Comeau和GCC)现在认为第二个模板更专业化。但为什么?
让我通过部分排序并展示我有疑问的地方。 Q 可以是用于根据14.5.5.2确定部分排序的唯一的虚构类型。
  • T1的转换后参数列表(插入Q):(Q,typename Const <Q> ::type *)。参数的类型为AT = (Q,void *)
  • T2的转换后参数列表(插入Q): BT = (Q,void *),它们也是参数的类型。
  • T1的非转换后参数列表:(T,typename Const <T> ::type *)
  • T2的非转换后参数列表:(T,void *)
由于C ++03未充分说明此问题,因此我使用了我在多个缺陷报告中读到的意图。上述T1的转换后参数列表(我称之为AT)用作14.8.2.1 “从函数调用推导模板参数”的参数列表。
对于每个 /

对独立执行类型推断,因此,14.8.2.1不需要自己转换ATBT(例如,删除引用声明符等),而是直接进入14.8.2.4进行类型推断。

  • ATT2{ (Q, T), (void*, void*) }。这里只有一个模板参数 T,它会发现 T 必须是 Q。对于 ATT2,类型推导很容易成功。

  • BTT1{ (Q, T), (void*, typename Const<T>::type*) }。这里也会发现 TQ。而 typename Const<T>::type* 是一个未推导的上下文,所以不会用于推导任何东西。


这是我的第一个问题:现在将使用为第一个参数推导出的 T 的值吗?如果答案是否定的,那么第一个模板更加特化。但这不可能是情况,因为GCC和Comeau都说第二个模板更加特化,而我不认为它们是错的。所以我们假设“是”,并将 void* 插入到 T 中。段落 (14.8.2.4) 说:推导是针对每个模板参数对独立进行的,然后将结果组合起来。在某些情况下,该值不参与类型推导,而是使用已在其他地方推断出或显式指定的模板参数的值。这也听起来像“是”。

因此,对于每个 A/P 对,推导都成功了。现在,每个模板至少与另一个一样特化,因为推导没有依赖于任何隐式转换,并且在两个方向上都成功了。因此,调用应该是不明确的。

所以我的第二个问题是:现在,为什么实现会说第二个模板更加特化?我忽略了什么要点吗?


编辑:我测试了显式的特化和实例化,在最近的GCC版本 (4.4) 中,它告诉我该特化的引用是不明确的,而较旧版本的GCC (4.1) 没有引发歧义错误。这表明最新的GCC版本具有不一致的函数模板部分排序。

template<typename T>
struct Const { typedef void type; };

template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

template<> void f(int, void*) { }
  // main.cpp:11: error: ambiguous template specialization 
  // 'f<>' for 'void f(int, void*)'
4个回答

7
这是我的翻译。我同意Charles Bailey的观点,错误的步骤是从Const::Type*转换为void*。请参考链接:Charles Bailey
template<typename T>
void f(T, typename Const<T>::type*) { cout << "Const"; } // T1

template<typename T>
void f(T, void*) { cout << "void*"; } // T2

我们要采取的步骤是:
14.5.5.2/2
给定两个重载的函数模板,可以通过依次转换每个模板并使用参数推断(14.8.2)将其与另一个进行比较来确定哪个更专业。
14.5.5.2/3-b1
对于每个类型模板参数,合成唯一的类型,并将其替换为函数参数列表中该参数的每个出现,或者对于模板转换函数,在返回类型中进行替换。
在我看来,类型的合成如下:
(Q, Const<Q>::Type*)    // Q1
(Q, void*)              // Q2

我没有看到任何要求 T1 的第二个合成参数必须是 void* 的措辞。在其他情况下,我也不知道有什么先例。类型 Const<Q>::Type* 是 C++ 类型系统中完全有效的类型。
因此,现在我们执行推断步骤: 从 Q2 推导到 T1 我们尝试为 T1 推导模板参数,因此我们有:
- 参数 1:将 T 推导为 Q - 参数 2:未推导的上下文
即使参数 2 是未推导的上下文,但推断仍然成功,因为我们有一个 T 的值。 从 Q1 推导到 T2 为 T2 推导模板参数,我们有:
- 参数 1:将 T 推导为 Q - 参数 2: void* 不匹配 Const<Q>::Type*,因此推导失败。
在我看来,这里的标准让我们失望了。该参数不是依赖项,因此不太清楚应该发生什么,但是我的经验(基于对 14.8.2.1/3 的阅读)是,即使参数类型 P 不是依赖项,那么实参类型 A 也应该与其匹配。
T1 的合成参数可以用于特化 T2,但反之则不行。因此,T2 比 T1 更专业化,因此是最佳函数。
更新 1: 只是为了涵盖关于 Const<Q>::type 是 void 的观点,请考虑以下示例:
template<typename T>
struct Const;

template<typename T>
void f(T, typename Const<T>::type*) // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T>
void f(T, void*)                    // T2
{ typedef typename T::TYPE2 TYPE ; }

template<>
struct Const <int>
{
  typedef void type;
};

template<>
struct Const <long>
{
  typedef long type;
};

void bar ()
{
  void * p = 0;
  f (0, p);
}

在上述代码中,当我们执行常规重载解析规则时,会使用Const<int>::type,但在进行部分重载规则时不会使用。选择任意特化的Const<Q>::type是不正确的。尽管这可能不直观,但编译器非常乐意使用形式为Const<Q>::type*的合成类型,并在类型推导期间使用它。请注意,保留了HTML标记。
template <typename T, int I>
class Const
{
public:
  typedef typename Const<T, I-1>::type type;
};

template <typename T>
class Const <T, 0>
{
public:
  typedef void type;
};

template<typename T, int I>
void f(T (&)[I], typename Const<T, I>::type*)     // T1
{ typedef typename T::TYPE1 TYPE; }

template<typename T, int I>
void f(T (&)[I], void*)                           // T2
{ typedef typename T::TYPE2 TYPE ; }


void bar ()
{
  int array[10];
  void * p = 0;
  f (array, p);
}

当使用某个值I实例化Const模板时,它会递归地实例化自身,直到I达到0。这是选择部分特化Const的时候。如果我们有一个编译器为函数参数合成一些真实类型,那么编译器会选择什么值作为数组下标呢?比如说10?对于上面的例子来说这是可以的,但它不符合部分特化Const,至少在概念上,这将导致主函数无限递归实例化。无论选取什么值,我们都可以修改结束条件为该值+1,然后我们就会在部分排序算法中得到一个无限循环。
我不知道部分排序算法如何正确实例化Const以找到type的真实类型。

然后 Const<Q>::Type* 将是 int*。但在我们的情况下(在这种情况下 :))),它是 void* - Johannes Schaub - litb
考虑以下形式的两个函数声明:void foo (typename Const<Q>::type)。编译器不应该查看Const的定义来查看type的真实情况,因此它必须创建一个内部的DependentNestedNameType。然后使用该类型来匹配foo等函数的声明。我认为在这种情况下,该过程非常相似。您不知道Q的真实情况,因此无法知道要选择哪个Const的特化。 - Richard Corden
1
@Richard,我刚刚检查了clang,它实际上并不合成任何类型!它只是使用带有依赖类型的原始函数模板参数列表。因此,它忽略了整个“替换为唯一类型、值或模板”的部分。“标准中的唯一类型”和“唯一值”似乎与“依赖类型”和“类型相关表达式”完全相同。不确定“唯一模板”对应什么,但这将解释为什么Const<Q>::type不被视为void。 - Johannes Schaub - litb
@Barry:当用某个值I实例化Const模板时,它将递归地实例化自身,直到I达到0。这时选择局部特化Const<T,0>。问题是编译器是否可以“发明”一个值用于局部排序?比如说10?对于上面的例子这是可以的,但它不会匹配局部特化Const<T, 10 + 1>,在概念上至少会导致无限数量的主递归实例化。 - Richard Corden
@RichardCorden 谢谢!我把那个评论加到了答案里 - 我觉得更清晰了。你知道自从你6年前发布这个答案以来有什么变化吗?还有一些关于这种事情的问题(1 2)。 - Barry
显示剩余14条评论

3

编辑:在研究Clang的部分排序算法实现(由Doug Gregor)之后,我开始同意其他帖子的观点,即原始示例并不是“有意”产生歧义的——尽管标准对这种情况应该发生什么并不像它本可以那样清晰。我已经编辑了这篇文章以表明我的修订想法(为了我的自身利益和参考)。特别是,Clang的算法澄清了在部分排序步骤中,“typename Const<T>::type”不会被翻译成“void”,每个A/P对是彼此独立推导的。

最初我想知道为什么以下内容被认为是模棱两可的:

        template<class T> void f(T,T*);  // 1

        template<class T> void f(T, int*); // 2

        f(0, (int*)0); // ambiguous

上述内容含糊不清,因为无法从f2(T,int*)推导出f1(U1,U1*),反过来也一样,无法从f1(T,T*)推导出f2(U2,int*)。两者都没有更具体的。但是以下内容不会有歧义:
        template<class T> struct X { typedef int type; };
        template<class T> void f(T, typename X<T>::type*); // 3
        template<class T> void f(T, int*); // 2

如果发生以下情况,则有理由期望它是模棱两可的:
- f3(U1,X<U1>::type*) -> f3(U1, int*) ==> f2(T,int*) (推断正确,T=U1)
- f2(U2,int*) ==> f3(T, X<T>::type*) (推断正确,T=U2使X<U2>::type* -> int*)
如果这是真的,那么两者都不会比另一个更加专业。
在研究Clang的部分排序算法后,很明显他们将上述“3”视为:
template<class T, class S> void f(T, S*); // 4

因此,将某些独特的“U”针对“typename X :: type”进行推断将成功 -

  • f3(U1,X <U1> :: type *)被视为f3(U1,U2 *)==> f2(T,int *)(推断不可行)
  • f2(U2,int *)==> f3(T,S * [[X<T> :: type *]])(推断可行,其中T = U2,S = int)

因此,“2”明显比“3”更具专业性。


好观点。我也不明白把 X<T> 放在中间会有什么区别。 - Johannes Schaub - litb

1
转换后的T1参数列表(插入Q):(Q,typename Const :: type *)。参数的类型为AT =(Q,void *)
我想知道这是否真的是正确的简化。当您合成类型Q时,是否允许为确定模板特化顺序而虚构Const的专门化?
template <>
struct Const<Q> { typedef int type; }

这意味着T2不至少与T1一样专业,因为void*参数与任何给定模板参数的T1第二个参数不匹配。

类型“Q”是由编译器合成的唯一类型,仅用于此目的(我认为这就是他们所说的“唯一”),并且没有名称。我们不能使用它来定义该特化。我也不确定我所做的简化是否有效。但是我们必须得到一个参数类型。因此,要查看typename Const<Q>::type的类型,必须查看Const<Q>。 “T1更加专业化,因为对于所有模板参数T,void*参数都无法用于T1的第二个参数。”:但是GCC和Comeau不同意这一点:(他们说T2更加专业化,...) - Johannes Schaub - litb
即使我为“int”设置了一个“Const”的特化,也是如此。 - Johannes Schaub - litb
你是对的,我错了。在14.5.5.2/4中,“至少同样专业”是通过允许从其他类型到void*的隐式转换来确定的。但我仍然不确定它们两者如何至少同样专业。 - CB Bailey
@litb:为什么你觉得“我们必须获得参数类型”?我认为这可能是逻辑错误的原因所在。 - Richard Corden
@Richard,因为没有类型,我们无法进行任何类型推导。所以 14.8.2.4/1 中说:“模板参数可以在几个不同的上下文中被推导出来,但在每种情况下,一个用模板参数指定的类型(称之为 P)将与一个实际类型(称之为 A)进行比较...”。Const<Q>::type 只是另一种语法(限定符-id),表示类型 void(简单类型说明符)。Const<Q>::type 也不是依赖项,因此这不能是“它还不等于任何其他类型,因为它是依赖项”的情况。 - Johannes Schaub - litb

1

编辑:请忽略此帖子 - 在研究Doug Gregor实现的clang部分排序算法(尽管在撰写本文时仅部分实现),似乎它将未推导的上下文视为另一个模板参数。这表明,具有显式void*参数的重载应该是更专业化的版本,并且不应存在歧义。像往常一样,Comeau是正确的。 至于标准中清楚定义此行为的措辞 - 那是另一回事...

由于此帖子也发布在comp.lang.c ++.moderated上,并且似乎也在那里引起了一些混乱 - 我认为我也会在这里发布对该组的答案 - 因为讨论显然与此处提出的问题相关。

7月25日,下午1:11,Bart van Ingen Schenau <b...@ingen.ddns.info>写道:

你这一步走得太快了。你怎么知道(编译器会不会知道)没有Const<Q>的特化,使得Const<Q>::type != void?

据我所知,编译器将A的参数列表转换为:AT=(Q,<unknown>*)。使用这些参数调用B需要一个隐式转换(<unknown>* to void*),因此A比B不专业。

我认为这是错误的。在部分排序期间检查哪个函数更专业时,编译器将参数列表转换为(Q, void*)-即实际上它实例化相关模板(最佳匹配),并在其中查找'type'的值-在这种情况下,基于主模板,它将是void *。

关于你提到的部分特化的问题 - 在比较哪个模板比另一个更特化时,唯一可以使用的类型是唯一生成的类型 - 如果在声明的实例化点(进行重载解析时)存在其他特化,则它们将被考虑。如果您稍后添加它们,并且它们应该被选择,您将违反ODR(根据14.7.4.1)。
部分/显式特化也会在候选集形成期间得到考虑 - 但这次是使用函数的实际参数类型。 如果X的最佳匹配部分特化导致函数类型对于某个参数具有更好的隐式转换序列, 那么我们永远不会进入部分排序阶段,那个“更好”的函数将被选择(在进入部分排序阶段之前)。
下面是一个示例,其中包含有关各个步骤应该发生的注释:
    template<class T, bool=true> struct X;  // Primary

    template<class T> struct X<T,true> { typedef T type; };  // A
    template<> struct X<int*,true> { typedef void* type; };  // B


    template<class T> void f(T,typename X<T>::type); //1
    template<class T> void f(T*,void*); //2


    int main()
    {
      void* pv;
      int* pi;


      f(pi,pi);   
      // two candidate functions: f1<int*>(int*,void*),  f2<int>(int*,void*)
      // Note: specialization 'B' used to arrive at void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1,true>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity 




      f(pv,pv);  
      // two candidate functions: f1<void*>(void*,void*), f2<void>(void*,void*)
      // Note: specialization 'A' used to arrive at second void* in f1
      // neither has a better ICS than the other, so lets partially order
      // transformed f1 is f1<U1>(U1,X<U1>::type) --> f1<U1>(U1,U1) 
      //       (template 'A' used to get the second U1)
      // obviously deduction will fail (U1,U1) -> (T*,void*)
      // and also fails the other way (U2*, void*) -> (T,X<T>::type)
      // can not partially order them - so ambiguity again             

    }

值得一提的是,如果主模板没有定义,则SFINAE在部分排序阶段操作,两者都无法推导出另一个,应该会产生歧义。
此外,如果添加另一个模板,如果将这些函数的任何一个实例化点移动到翻译单元中的其他位置,那么您将明显违反ODR。
引用块:
“首先,更专业意味着可以通过重载解析选择较少的类型。使用这个,可以总结出部分排序的规则:尝试找到一个类型A,使得可以调用A但不能调用B,或者重载解析更喜欢调用A。如果可以找到该类型,则B比A更专业。”
在这里没有争议。但基于当前的规则,OP的示例必须是有歧义的。

最后,这里是对litb提出的两个具体问题的明确、无歧义的答案:

1)现在会使用为第一个参数推导出的T的值吗?
是的 - 当然,它必须这样做,因为它正在进行模板参数推断 - “链接”必须得到维护。

2)那么,为什么实现中说第二个更专业呢?
因为他们是错的 ;)

我希望这解决了这个问题 - 如果还有什么不清楚的,请让我知道 :)

编辑: litb在评论中提出了一个很好的观点 - 或许声明主模板将始终使用 用于唯一生成类型的实例化是太强了。
在某些情况下,主模板将不被调用。
我想说的是,在进行部分排序时,会使用一些独特的生成类型来匹配最佳专业化。 你是对的,它不一定是主模板。 我已经编辑了上述语言以做到这一点。 他还提出了关于在实例化点之后定义更好匹配模板的问题。 根据有关实例化点的部分,这将违反ODR。


标准规定,一旦使用temp.func.order中描述的转换规则创建了A/P对,它们将使用模板参数推导(temp.deduct)相互推导,并且该部分处理非推导上下文的情况,实例化模板及其嵌套类型,触发实例化点。temp.point部分处理ODR违规(偏序的含义不应因翻译单元内的实例化点而改变)。我仍然不确定混淆来自何处?- Faisal Vali 1小时前[删除此评论] litb:“请注意,将Q放入Const :: type以构建参数的步骤未明确涵盖SFINAE规则。 SFINAE规则使用参数推导,但将Q放入函数模板函数参数列表的段落在14.5.5.2处。”
必须在这里使用SFINAE规则-它们怎么可能不使用呢? 我认为已经足够暗示了-我不会否认它可能更清晰,虽然我鼓励委员会澄清这一点-但我认为不需要澄清就可以充分解释您的示例。

让我提供一个将它们连接起来的方法。 从(14.8.2): “当指定显式模板参数列表时,模板参数必须与模板参数列表兼容,并且必须产生有效的函数类型,如下所述;否则类型推断失败”

从(14.5.5.2/3) 使用的转换是: 对于每个类型模板参数,合成唯一类型,并将其替换为函数参数列表中的每次出现,或者对于模板转换函数,在返回类型中。

在我看来,上面的引用意味着一旦为每个模板参数“创建”了独特的生成类型,函数声明就必须通过显式地提供唯一类型作为我们的函数模板的模板参数来隐式实例化。如果这导致无效的函数类型,那么不仅是转换,更重要的是后续的模板参数推导也会部分有序地失败。

从(14.5.5.2/4) “使用转换的函数参数列表,针对另一个函数模板执行参数推断。如果推断成功并且推断的参数类型是精确匹配的(因此推断不依赖于隐式转换),则变换的模板至少与其他模板一样专业。”

如果转换后的函数参数列表导致替换失败,那么我们知道推断不可能成功。既然推断没有成功,它就不像另一个那样专业化 - 这就是我们需要知道的,以便在部分排序这两个函数时继续进行。

litb: 我也不确定在这种情况下会发生什么:template<typename T> struct A; template<typename T> void f(T, typename A<T>::type); template<typename T> void f(T*, typename A<T>::type); 当然, 这是有效的代码,但做A :: type时,它将失败,因为在模板定义上下文中,A尚未定义" 还要注意的是,对于由此类替换导致的模板实例化,没有为其定义POI(偏序不依赖于任何上下文。它是涉及两个函数模板的静态属性)。 我认为这看起来像是标准中需要修复的问题。

好的 - 我认为我明白我们在哪里看法不同。如果我理解你正确,你是说当这些函数模板被声明时, 编译器会跟踪它们之间的部分排序,而不管重载决议是否被触发以在它们之间进行选择。 如果你是这样解释它的话,那么我可以理解为什么你会期望描述的上述行为。 但我不认为标准要求或强制执行这种情况。

现在,标准明确指出,部分排序与调用函数时使用的类型无关(我相信这就是你描述它为静态属性且与上下文无关时所指的内容)。

标准还明确表示,它仅在过载决议的过程中(13.3.3/1)以及仅在ICS无法选择更好的函数或其中一个是模板而另一个不是时才关心函数模板之间的部分排序(调用部分排序)。[类模板部分特化的部分排序是一个单独的问题,在我看来,它使用了需要实例化该特定类的相关上下文(其他模板定义)]。

因此,在我看来,由于函数模板的部分排序机制在执行重载解析时被调用,它必须使用在执行重载解析时可用的上下文(模板定义和特化)的相关部分。
所以根据我的理解,根据您上面使用“template struct A”的示例,该代码是有效的。部分排序不是在定义上下文中完成的。但是,如果/当您通过编写对f((int *)0,0)的调用在两个函数之间调用重载解析时 - 并且在编译器在那时尝试组装候选声明或部分排序它们(如果它到达了部分排序步骤),如果作为函数类型的一部分产生无效的表达式或类型,则SFINAE会帮助我们并告诉我们模板推断失败(就部分排序而言,这意味着如果我们甚至不能转换模板,则不能比另一个更专业化)。
现在关于POI - 如果您像我一样相信,转换后的函数类型应该表示使用显式提供的模板参数列表(使用唯一生成的类型)的隐式实例化,则以下标准引用是相关的:

14.6.4.1/1 对于函数模板特化、成员函数模板特化,或者类模板的成员函数或静态数据成员的特化,如果特化是因为它被另一个模板特化引用而隐式实例化,并且引用它的上下文取决于模板参数,则特化的实例化点是封闭特化的实例化点。

我理解的意思是,变换后的函数类型和原始函数类型的实例化点与通过实际函数调用创建的函数的实例化点相同。

litb: 由于部分排序只是参数语法形式的属性(即“T *”与“T(*)[N]”),我会投票修订规范(例如,“如果Q出现在限定符命名类型的限定符ID的嵌套名称中,则命名的类型为“Q”)或者说命名的类型是另一种唯一的类型。这意味着在template void f(T, typename Const::type*)中,参数列表为(Q, R*),例如。对于template void f(T*, typename ConstI::type)也是如此,arg lisst将是(Q*, R)。当然,非类型参数也需要类似的规则。虽然我需要考虑一下并制作一些测试用例来看看这是否会产生自然排序。

啊 - 现在你提出了一个可能的解决方案,以有利于我们所有人直观地期望的方式解决了歧义 - 这是一个单独的问题,虽然我喜欢你所走的方向,但像你一样,我也需要仔细考虑它是否可行。

感谢您继续讨论。我希望 SO 不仅限于您发表评论。

由于您可以编辑我的帖子,请随意在帖子中回复,如果这更容易的话。


为什么它总是使用主模板?你有标准的报价吗?请考虑:template<typename T, bool = true> struct X; template<typename T> struct X<T, true> { typedef void *type; }; 在这种情况下,部分特化匹配并将用于 Q。我认为 Bat van Ingen 想知道的是,在函数模板定义之后是否存在另一个特化,会发生什么。部分排序无法考虑它,因为它还不知道它。但稍后对特化的引用将考虑它。 - Johannes Schaub - litb
但是我没有看到标准中对于这种情况的任何说明。 :( - Johannes Schaub - litb
请注意,将Q放入Const<T>::type以构建参数的步骤并未明确涵盖SFINAE规则。 SFINAE规则适用于参数推导,将Q放入函数模板函数参数列表的段落在14.5.5.2处。 我也不确定在此情况下会发生什么:template<typename T> struct A; template<typename T> void f(T, typename A <T> ::type); template<typename T> void f(T *, typename A <T> ::type);当然,这是要成为有效代码的,但是使用A<Q>::type时,它将失败,因为在模板定义上下文中,A尚未定义 - Johannes Schaub - litb
好的,我编辑了我的帖子以回应你所提出的一些优秀评论。 - Faisal Vali
C++的类型系统能够处理涉及依赖类型的特化(实际上它需要这样做)。因此,“Const<Q>::Type”无需实例化为“void”。 - Richard Corden
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接