代码之家  ›  专栏  ›  技术社区  ›  Johannes Schaub - litb

具有未减少上下文的函数模板的部分排序

  •  22
  • Johannes Schaub - litb  · 技术社区  · 15 年前

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

    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,空隙*) ,这也是参数的类型。
    • 的未转换参数列表 T1 : (T, typename Const<T>::type*)
    • 的未转换参数列表 T2 : (T, void*)

    由于C++ 03指定了这一点,所以我使用了我在几个缺陷报告中读到的意图。上面转换的参数列表 T1 (被称为 AT 被我)用作 14.8.2.1 “从函数调用推断模板参数” .

    14.2.1 不需要转换 AT 英国电信 它本身(例如,删除引用声明符等),然后直接转到 14.8.2.4 分别为每个 A / P 配对进行类型推导:

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

    • 英国电信 反对 T1 : { (q,t) , (void*, typename Const<T>::type*) } . 它会发现 T Q 也在这里。 typename Const<T>::type* 是一个未推导的上下文,因此它不会用于推导任何内容。


    这是我的第一个问题:这是否会用到 T 为第一个参数推导?如果答案是否定的,那么第一个模板就更专业化了。但情况并非如此,因为GCC和Comeau都说第二个模板更专业,我不认为它们是错误的。因此我们假设“是”,并插入 void* 进入之内 T . 段落( 14.2.4 “对每对单独进行扣除,然后合并结果” 而且 但是,在某些上下文中,该值不参与类型推导,而是使用在别处推导或显式指定的模板参数的值。 这听起来也像是“是”。

    因此,每对应付账款的扣除也会成功。现在,每个模板至少和其他模板一样专门化,因为推导也不依赖于任何隐式转换,并且在两个方向上都成功了。因此,调用应该是不明确的。

    所以我的第二个问题是:现在,为什么实现说第二个模板更专业化?我忽略了什么?


    编辑 :我在最新的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 回复  |  直到 6 年前
        1
  •  6
  •   Community THelper    7 年前

    这是我的任务。我同意 Charles Bailey 错误的步骤是从 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
    

    我们要采取的步骤是:

    145.5.2/2

    对于两个重载的函数模板,可以通过依次转换每个模板并使用参数推导(14.8.2)将其与另一个模板进行比较来确定一个模板是否比另一个更专门化。

    145.5.2/3-B1

    对于每个类型模板参数,合成一个唯一的类型,并将其替换为函数参数列表中该参数的每次出现,或者替换为返回类型中的模板转换函数。

    在我看来,这些类型的合成如下:

    (Q, Const<Q>::Type*)    // Q1
    (Q, void*)              // Q2
    

    我看不出任何措辞要求 T1 空洞* . 在其他情况下,我也不知道有什么先例。类型 常量:类型* 在C++类型系统中是完全有效的类型。

    因此,现在我们执行扣除步骤:

    Q2到T1

    我们尝试推导T1的模板参数,因此我们得出:

    • 参数1: T 被推断为 Q
    • 参数2:非导出上下文

    即使参数2是非推导上下文,推导仍然成功,因为我们有一个T的值。

    Q1到T2

    推导出t2的模板参数,我们有:

    • 参数1: T 被推断为 Q
    • 参数2: 空洞* 不匹配 常量:类型* 所以演绎失败。

    imho,这是标准让我们失望的地方。参数不依赖,所以不清楚应该发生什么,但是,我的经验(基于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* 并在类型推导过程中使用。


    更新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 模板是用某个值实例化的 I ,它递归地实例化自己,直到 达到0。这是当部分专业化 Const<T,0> 被选中。如果我们有一个为函数参数合成某种实数类型的编译器,那么编译器将为数组索引选择什么值?比如说10?好吧,对于上面的例子来说这是可以的,但是它不符合部分专门化 Const<T, 10 + 1> 至少在概念上,这会导致无限多的主实例递归。无论它选择了什么值,我们都可以将结束条件修改为该值+1,然后在部分排序算法中有一个无限循环。

    我不知道部分排序算法如何正确地实例化 康斯特 寻找什么 type 真的是。

        2
  •  2
  •   Faisal Vali    15 年前

    编辑:学习后 Clang's (由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”更专业。

        3
  •  1
  •   CB Bailey    15 年前

    T1(Q)的转换参数列表 插入):(q,类型名 Const::类型*)。的类型 参数位于=(q,void*)

    我想知道这是否是一个正确的简化。当你合成类型 Q ,你能想出一个专门的 Const 为了确定模板规格化的顺序?

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

    这意味着 T2 至少不像 T1 因为A void* 参数不匹配 T1 任何给定模板参数的第二个参数。

        4
  •  1
  •   Faisal Vali    15 年前

    编辑:请忽略这篇文章-在研究了道格·格雷戈(Doug Gregor)实现的部分排序的clangs算法之后(尽管它在本文中只部分实现了一部分-似乎与OP问题相关的逻辑已经充分实现了)-它似乎将未减少的上下文视为另一个TE。MPlate参数。这表明显式void*参数的重载应该是更专门化的版本,不应该有歧义。像往常一样,科莫是正确的。 至于标准中明确定义这种行为的措辞,那是另一回事……

    因为这篇文章也是在comp.lang.c++上发表的,而且似乎也引起了一些混乱——我想我也会把我的答案贴在这里——因为讨论显然与这里提出的问题有关。

    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*) -也就是说,它实际例示了 模板(最佳匹配)并在其内部查找“type”的值-在本例中,基于 在主模板上,它将是无效的*。

    关于你关于部分专业化的观点-当检查 哪个模板比另一个模板更专业化,这是唯一可以使用的类型 是唯一生成的类型-如果此时有其他专门化 声明的实例化(当正在完成重载解析时) 他们将被考虑。如果您以后添加它们,它们应该被选中 您将违反ODR(根据14.7.4.1)

    在 候选集的形成-但这次使用的是实际参数的类型 到函数。如果(x的)最佳匹配部分专门化导致 对于某些函数类型具有更好的隐式转换序列 参数,那么我们永远不会到达部分排序阶段,并且 “Better”(更好)功能将被选中(在将其设置为部分之前 订购阶段)

    下面是一个示例,其中包含有关在各个步骤中应执行的操作的注释:

        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。

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

    首先,更加专业意味着 较少的 类型在哪里 该模板可以通过过载分辨率进行选择。 这样,部分排序的规则可以概括为:尝试 查找的类型,使a可以被调用,但b不能调用,或重载 分辨率更倾向于调用A。如果可以找到该类型,则B更多 比A专业。

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


    最后,下面是对LITB提出的两个具体问题的明确、明确的答案:

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

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

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

    编辑: Litb在他的评论中提出了一个很好的观点——也许他说主要模板总是 用于唯一生成类型的实例化的语句太强。
    有些实例将不调用主模板。
    我得到的是,当发生部分排序时,一些唯一的生成类型是 用于匹配最佳专业。你说得对,它不一定是主模板。 为此,我编辑了上述语言。 他还提出了一个关于在实例化点之后定义更好的匹配模板的问题。 根据实例化点部分,这将违反ODR。


    标准规定,一旦创建了A/P对(使用temp.func.order中描述的转换规则),就可以使用模板参数推导(temp.reduce)对它们进行推导,该部分处理非推导上下文的情况,实例化模板及其嵌套类型,触发实例化点。.temp.point部分处理ODR冲突(不管翻译单元中的实例化点如何,部分排序的含义都不应该改变)。我还是不知道混乱是从哪里来的?_“Faisal Vali 1小时前[删除此评论]

    litb:“请注意,将q放入const::type以构建参数的步骤没有被sfinae规则明确覆盖。 sfinae规则使用参数推导,将q放入函数模板函数参数列表的段落放在14.5.5.2'处。

    这里必须使用斯芬纳规则——怎么可能不使用呢? 我觉得这是充分的暗示-我不会否认它可能更清楚,虽然我鼓励委员会澄清 这-我认为不需要澄清来充分解释您的示例。

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

    自(14.5.5.2/3) “使用的转换是: _ 函数参数列表中的参数,或返回类型中的模板转换函数的参数。“

    在我看来,上面的引用意味着一旦为每个模板参数“创建”了唯一的生成类型,函数声明就必须 关联性由实例化 明确地 将唯一类型作为模板参数提供给我们的函数模板。如果这导致无效 函数类型,那么不仅是转换,更重要的是后续的模板参数推导需要 部分排序功能失败。

    自(14.5.5.2/4) “使用转换后的函数参数列表,对其他函数模板进行参数推导。转换后的模板 至少和其他人一样专业 如果且仅当 ,演绎成功,演绎参数类型 是完全匹配的(因此推导不依赖于隐式转换)。”

    如果转换后的函数参数列表导致替换失败,那么我们就知道演绎是不可能成功的。 既然演绎没有成功,它就没有另一个那么专业了——这就是我们继续下去所需要知道的一切。 在部分排序的两个。

    利特:我也不确定在这种情况下会发生什么: 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); 当然, 这是不确定的有效代码,但执行::type时,它将失败,因为 模板定义上下文,a尚未定义“ 还要注意,没有为模板实例化定义POI 尝试确定顺序时的替换类型(部分顺序不依赖于 任何上下文。它是涉及两个函数模板的静态属性)。 我认为这是标准中的一个问题,需要解决。

    好吧-我想我看到了我们不同的看法。如果我正确理解你,你就是在说 当这些函数模板被声明时,编译器会跟踪它们之间的部分排序, 无论过载分辨率如何,都会被触发在它们之间进行选择。 如果这就是你解释它的方式,那么我可以理解为什么你会期望你描述的上述行为。 但我认为该标准从来没有要求或要求这样做。

    现在,标准已经明确了部分排序对于调用函数的类型是不可知的(我相信 当您将其描述为静态属性并且与上下文无关时,这就是您所指的内容)。

    该标准还清楚地表明,它只关心函数模板之间的部分排序(调用部分排序)。 在过载分辨率(13.3.3/1)过程中,如果且仅当它不能根据IC或 如果一个是模板,另一个不是。[类模板的部分排序部分专用化是一个单独的问题 在我看来,使用相关的上下文(其他模板定义),它需要实例化该特定类。]

    因此,在我看来,因为函数模板的部分排序机制是在重载时调用的 执行解析时,必须使用上下文的相关部分(模板定义和专门化)。 在完成过载分辨率时。

    基于我的兴趣,根据上面使用“template struct a”的示例,代码是有效的。 部分排序不是在定义上下文中完成的。但如果/当您碰巧调用重载解决方案时 通过写入对f((int*)0,0)的调用,在两个函数之间,当编译器 尝试组装候选声明或对其部分排序(如果到达部分排序步骤) 如果一个无效的表达式或类型作为函数类型的一部分产生,sfinae将帮助我们解决问题并告诉我们 我们认为模板推导失败(就部分排序而言,这意味着 如果我们甚至不能转换模板,就不能比另一个更专门化)。

    现在关于poi——如果你像我一样确信转换函数类型应该 使用显式提供的模板参数列表(使用唯一生成的类型)表示隐式实例化 那么以下标准报价是相关的:

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

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

    因为部分排序只是 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. 不过,我必须考虑一下并做一些测试用例,看看这是否会产生自然的顺序。

    啊-现在你提出了一个可能的解决方案,它解决了歧义,有利于我们 所有这些都是直觉上的期望——这是一个单独的问题,虽然我喜欢你前进的方向, 像你一样,在宣布它的可操作性之前,我也必须先考虑一下。

    感谢您继续讨论。我希望这样做不只是限制你发表评论。

    既然你可以编辑我的帖子,如果方便的话,请随时在帖子内回复。