24

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

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 没有明确说明这一点,我确实使用了我在几个缺陷报告中读到的意图。上面转换的参数列表T1AT由我调用)用作14.8.2.1 “从函数调用中推导出模板参数”的参数列表。

14.8.2.1不再需要转换ATorBT本身(例如,删除引用声明符等),并直接转到14.8.2.4,对于每个A/P对独立地进行类型推导:

  • AT反对T2:。是这里唯一的模板参数,它会发现必须是. 类型推导对于against轻而易举地成功。{ (Q, T), (void*, void*) }TTQATT2

  • BT反对T1:。它会发现is , too here。是一个未推断的上下文,因此它不会用于推断任何内容。{ (Q, T), (void*, typename Const<T>::type*) }TQtypename Const<T>::type*


这是我的第一个问题:现在这将使用Tdeduced 的值作为第一个参数吗?如果答案是否定的,那么第一个模板更专业。这不可能,因为 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

4 回答 4

6

这是我的尝试。我同意查尔斯·贝利的观点,错误的步骤是从Const<Q>::Type*void*

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

我没有看到任何要求T1be的第二个综合参数的措辞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无效的观点。考虑以下示例:

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*并在类型推导期间使用它。


更新 2

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);
}

Const模板用某个 value 实例化时I,它会递归地实例化自己,直到I达到 0。这是Const<T,0>选择部分特化的时候。如果我们有一个编译器为函数的参数合成一些真实类型,那么编译器将为数组索引选择什么值?说10?好吧,这对于上面的例子来说很好,但它与部分特化不匹配Const<T, 10 + 1>,至少从概念上讲,这将导致无限数量的主递归实例化。无论它选择什么值,我们都可以将结束条件修改为该值 + 1,然后我们将在偏序算法中有一个无限循环。

我看不到部分排序算法如何正确实例化Const以找到type真正的东西。

于 2009-07-28T11:06:49.913 回答
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

(The above is ambiguous because one cannot deduce f1(U1,U1*) from f2(T,int*), and going the other way, one cannot deduce f2(U2,int*) from f1(T,T*). Neither is more specialized.)

但以下内容不会模棱两可:

        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*) (deduction ok, T=U1)
-f2(U2,int*) ==> f3(T, X<T>::type*) (deduction ok, T=U2 makes X<U2>::type* -> int*)
如果这是真的,那么任何一个都不会比另一个更专业。)

在研究了 Clang 的部分排序算法之后,很明显他们将上面的 '3' 视为:

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

所以对 'typename X::type' 扣除一些独特的 'U' 会成功 -

  • f3(U1,X<U1>::type*) is treated as f3(U1, U2*) ==> f2(T,int*) (deduction not ok)
  • f2(U2,int*) ==> f3(T,S* [[X<T>::type*]]) (deduction ok, T=U2, S=int)

所以'2'显然比'3'更专业。

于 2009-07-25T18:30:16.400 回答
1

T1 的转换参数列表(Q 插入):(Q,typename Const::type*)。参数的类型是 AT = (Q, void*)

我想知道这是否真的是一个正确的简化。当你综合 typeQ时,你是否允许Const为了确定模板规范化的顺序而想出一个专门化?

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

这意味着它T2至少不像任何给定模板参数T1void*参数不匹配的第二个参数那样专门化。T1

于 2009-07-24T22:39:42.907 回答
1

编辑:请忽略这篇文章 - 在研究了 Doug Gregor 实现的部分排序的 clangs 算法之后(尽管在撰写本文时它只是部分实现了 - 似乎与 OP 的问题相关的逻辑已经足够充分地实现了) - 它看起来好像它只是将未推断的上下文视为另一个模板参数。这表明带有显式 void* 参数的重载应该是更专业的版本,并且不应该有歧义。像往常一样,科莫是正确的。现在至于标准中明确定义这种行为的措辞 - 那是另一回事......

由于这篇文章也发布在 comp.lang.c++.moderated 上,并且似乎也在那里引起了一些混乱——我想我也会在这里向那个小组发布我的答案——因为讨论显然与这里提出的问题有关.

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

You are going one step too fast here. How do you know (and would the compiler know) that there is no specialisation of Const<Q> such that Const<Q>::type != void?

As far as I can see, the compiler would transform the parameter-list of A to: AT=(Q, <unknown>*). To call B with these parameters requires an implicit conversion (<unknown>* to void*) and therefore A is less specialised than B.

我相信这是不正确的。当检查哪个函数更专业时(在部分排序期间),编译器将参数列表转换为(Q, void*)- 即它实际上实例化相关模板(最佳匹配)并在其中查找“类型”的值 - 在这个情况下,基于主模板,它将是 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。

On Jul 25, 1:11 pm, Bart van Ingen Schenau <b...@ingen.ddns.info> wrote:

首先,更专业意味着这些模板可以通过重载决议选择的类型更少。使用这一点,偏序的规则可以总结为:尝试为 A 找到一个可以调用 A 但不能调用 B 的类型,或者重载决策更喜欢调用 A。如果可以找到该类型,则 B 更专业比 A。

这里没有争论。但是根据目前的规则,OP 的示例必须是模棱两可的。


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

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

2)现在,为什么实现说第二个更专业呢?
因为他们错了;)

我希望这可以解决这个问题 - 如果还有什么不清楚的地方请告诉我:)

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


该标准说,一旦创建了 A/P 对(使用 temp.func.order 中描述的转换规则),它们就会使用模板参数推导(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 选择更好的函数或者如果一个是一个模板,另一个不是。[类模板部分特化的部分排序是一个单独的问题,在我看来,使用需要实例化该特定类的相关上下文(其他模板定义)。]

因此,在我看来,由于在执行重载解析时调用了函数模板的部分排序机制,因此它必须使用在完成重载解析时可用的上下文的相关部分(模板定义和特化) .

因此,根据我的解释,根据您上面使用“模板结构 A”的示例,代码是有效的。部分排序不是在定义上下文中完成的。但是,如果/当您碰巧通过编写对 f((int*)0,0) 的调用来调用两个函数之间的重载解决方案时 - 并且在编译器尝试组装候选声明或部分排序它们时(如果它进入偏序步骤)如果一个无效的表达式或类型作为函数类型的一部分产生,SFINAE 帮助我们并告诉我们模板推导失败(就偏序而言,这意味着一个不能更多如果我们甚至无法转换模板,则比其他更专业)。

现在关于 POI - 如果您像我一样确信转换后的函数类型应该使用显式提供的模板参数列表(使用唯一生成的类型)来表示隐式实例化,那么以下标准引号是相关的:

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

我对此的解释是,转换后的函数类型和原始函数类型的 POI 与实际函数调用创建的那些函数的 POI 相同。

litb:由于部分排序只是 a property of the syntactic form of parameters (i.e "T*" against "T(*)[N]"), 我会投票修改规范(比如“如果 Q 出现在命名类型的限定 ID 的嵌套名称说明符中,那么命名的类型是“Q”)或者说命名的类型是另一种独特的类型。 This means that in template<typename T> void f(T, typename Const<T>::type*); the argument list is (Q, R*), for example. Same for template<typename T> void f(T*, typename ConstI<sizeof(T)>::type); the arg lisst would be (Q*, R). A similar rule would be needed for non-type parameters, of course. 不过,我将不得不考虑它并制作一些测试用例,看看这是否会产生自然排序。

啊-现在您正在建议一种可能的解决方案,以解决我们所有人直观期望的歧义-这是一个单独的问题,虽然我喜欢您前进的方向,就像您一样,但我也不得不考虑一下在宣布它的可操作性之前进入它。

感谢您继续讨论。我希望 SO 不只是限制您发表评论。

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

于 2009-07-26T16:07:51.140 回答