【问题标题】:Why is this ambiguity here?为什么这里会出现这种歧义?
【发布时间】:2011-03-31 23:05:46
【问题描述】:

考虑我有以下最少的代码:

#include <boost/type_traits.hpp>

template<typename ptr_t>
struct TData
{
    typedef typename boost::remove_extent<ptr_t>::type value_type;
    ptr_t data;

    value_type & operator [] ( size_t id ) { return data[id]; }
    operator ptr_t & () { return data; }
};

int main( int argc, char ** argv )
{
    TData<float[100][100]> t;   
    t[1][1] = 5;
    return 0;
}

GNU C++ 给了我错误:

test.cpp: In function 'int main(int, char**)':
test.cpp:16: error: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for second:
test.cpp:9: note: candidate 1: typename boost::remove_extent<ptr_t>::type& TData<ptr_t>::operator[](size_t) [with ptr_t = float [100][100]]
test.cpp:16: note: candidate 2: operator[](float (*)[100], int) <built-in>

我的问题是:

  1. 为什么 GNU C++ 给出错误,而 Intel C++ 编译器却没有?
  2. 为什么将operator[] 更改为以下会导致编译没有错误?

    value_type &amp; operator [] ( int id ) { return data[id]; }

感谢 C++ 标准的链接。


我可以看到这里有两种转化路径:

  1. (1)intsize_t 和 (2)operator[](size_t)
  2. (1)operator ptr_t&amp;()、(2)intsize_t 和(3)内置operator[](size_t)

【问题讨论】:

标签: c++ templates operator-overloading


【解决方案1】:

其实很简单。对于t[1],重载决议有以下候选:

候选 1(内置:13.6/13)(T 是任意对象类型):

  • 参数列表:(T*, ptrdiff_t)

候选人 2(您的接线员)

  • 参数列表:(TData&lt;float[100][100]&gt;&amp;, something unsigned)

参数列表由13.3.1.2/6给出:

重载决策的候选函数集是成员候选函数、非成员候选函数和内置候选函数的并集。参数列表包含运算符的所有操作数。

  • 参数列表:(TData&lt;float[100][100]&gt;, int)

您看到第一个参数与候选 2 的第一个参数完全匹配。但它需要对候选 1 的第一个参数进行用户定义的转换。因此对于第一个参数,第二个候选者获胜。

您还看到第二个位置的结果取决于。让我们做一些假设,看看我们得到了什么:

  1. ptrdiff_tint:第一个候选者获胜,因为它具有完全匹配,而第二个候选者需要整数转换。
  2. ptrdiff_tlong:两个候选人都没有获胜,因为两者都需要整数转换。

现在,13.3.3/1

令ICSi(F)表示将列表中的第i个参数转换为可行函数F的第i个参数的类型的隐式转换序列。

如果对于所有参数 i,ICSi(F1) 不是比 ICSi(F2) 更差的转换序列,则一个可行函数 F1 被定义为比另一个可行函数 F2 更好的函数,然后 ... 对于某个参数 j , ICSj(F1) 是比 ICSj(F2) 更好的转换序列,或者,如果不是...

对于我们的第一个假设,我们没有获得总冠军,因为候选人 2 赢得了第一个参数,而候选人 1 赢得了第二个参数。我称之为纵横交错。对于我们的第二个假设,候选 2 总体上获胜,因为这两个参数的转化率都较差,但第一个参数的转化率更好

对于第一个假设,第二个参数中的整数转换(int 到 unsigned)与用户定义的第一个参数中另一个候选者的转换相比,这并不重要。在纵横交错中,规则是粗糙的。


最后一点可能会让你感到困惑,因为大家都在大惊小怪,所以让我们举个例子

void f(int, int) { }
void f(long, char) { }

int main() { f(0, 'a'); }

这给了你同样令人困惑的 GCC 警告(我记得,当我几年前第一次收到它时,它实际上把我搞糊涂了),因为 0 转换为 long'a' 更糟糕int - 但你会感到模棱两可,因为你处于纵横交错的境地。

【讨论】:

  • 我有一个疑问:你如何确定哪个匡威更差? intlongcharint 差,为什么不反过来呢?
  • @nawaz 因为它是由 thenspec 以这种方式定义的。请参阅积分促销和/与积分转换。
【解决方案2】:

用表达式:

t[1][1] = 5;

编译器必须关注左侧以确定那里的内容,因此= 5; 将被忽略,直到 lhs 得到解决。给我们留下表达式:t[1][1],它代表两个操作,第二个操作对第一个的结果进行操作,因此编译器必须只考虑表达式的第一部分:t[1]。实际类型是(TData&amp;)[(int)]

调用与任何函数都不完全匹配,因为TDataoperator[] 被定义为采用size_t 参数,因此为了能够使用它,编译器必须将1int 转换到 size_t 的隐式转换。那是第一选择。现在,另一种可能的途径是应用用户定义的转换将TData&lt;float[100][100]&gt; 转换为float[100][100]

intsize_t 转换是一个整体转换,在标准的表 9 中被列为 Conversion,如是根据§13.3.3.1.2/4 从TData&lt;float[100][100]&gt;float[100][100]用户定义的转换。从float [100][100]&amp;float (*)[100] 的转换在表9 中被列为完全匹配。编译器不允许从这两个转换序列中进行选择。

Q1:并非所有编译器都以相同的方式遵守标准。发现在某些特定情况下编译器的执行方式与其他编译器不同,这是很常见的。在这种情况下,g++ 实现者决定抱怨标准不允许编译器选择,而英特尔实现者可能只是默默地应用了他们喜欢的转换。

Q2:当您更改用户定义的operator[] 的签名时,参数与传入的类型完全匹配。 t[1]t.operator[](1) 完美匹配,没有任何转换,因此编译器必须遵循该路径。

【讨论】:

  • 我认为您可以在第二段中简化此答案。你根本不用看第二个[]。您只需要考虑如何解释t[1]。类型类型为TData(左值)和int(右值)。
  • @Charles:我意识到第二对[] 没有考虑,因为它取决于第一个结果。
  • 表 9 仅适用于标准转换序列 - “表 9 中的每个转换还具有关联的排名(精确匹配、提升或转换)。这些用于对标准转换序列进行排名 (13.3.3.2 )。”。那么这个逻辑真的成立吗?
  • 13.3.3.1.2/4 指的是基类转换。 float[100][100] 不是 TData&lt;&gt; 的基类,所以这是一条红鲱鱼。
  • 另外,@chubsdad 是正确的:转换函数是用户定义的转换,而 intsize_t 是标准转换,最好根据 13.3.3.1/3。跨度>
【解决方案3】:

我不知道确切的答案是什么,但是...

因为这个运算符:

operator ptr_t & () { return data; }

已经存在接受size_t 作为索引的内置[] 运算符(数组订阅)。所以我们有两个[] 运算符,内置的和你定义的。 Booth 接受size_t,因此这可能被视为非法超载。

//编辑
这应该可以按您的预期工作

template<typename ptr_t>
struct TData
{
    ptr_t data;
    operator ptr_t & () { return data; }
};

【讨论】:

    【解决方案4】:

    在我看来,

    t[1][1] = 5;
    

    编译器必须在两者之间做出选择。

    value_type & operator [] ( size_t id ) { return data[id]; }
    

    如果将int 文字转换为size_t,则匹配,或者

    operator ptr_t & () { return data; }
    

    后跟普通数组索引,在这种情况下索引的类型完全匹配。


    至于错误,GCC 作为编译器扩展似乎想为您选择第一个重载,并且您正在使用 -pedantic 和/或 -Werror 标志进行编译,这迫使它遵守标准的字眼.

    (我没有学究气,所以没有引用标准,特别是在这个话题上。)

    【讨论】:

    • 为什么在第二种情况下没有intsize_t的转换? normal array indexing 需要什么类型?我也觉得应该是size_t
    • @Kirill V. Lyadvinsky:不,对于指针类型和整数类型的每种组合都有一个内置的operator[]int 无需转换即可工作。
    • @Charles: 13.6/13: “存在形式的候选运算符函数...... T&amp; operator[](ptrdiff_t,T*);”所以不,ptrdiff_t(不是 size_t)是特殊的。 size_tint 都需要转换。 (不过,这里更相关的是调用基里尔的“修复”重载时缺乏转换。)
    • @Potatoswatter:我的立场是正确的。我正在查看加法运算符的定义,它在“指针加整数”情况下不区分整数类型。我不知道关于重载解析的部分让 ptrdiff_t 变得特别。
    • @Potatoswatter:虽然int 可能不需要转换;据我所知,ptrdiff_t 在某些实现上可能是int 的有效类型定义。
    【解决方案5】:

    我试图展示表达式 t[1][1] 的两个候选者。这些都是相等的 RANK (CONVERSION)。因此模棱两可

    我认为这里的问题是根据 13.6/13 的内置 [] 运算符定义为

    T& operator[](T*, ptrdiff_t);
    

    在我的系统上,ptrdiff_t 被定义为“int”(这可以解释 x64 行为吗?)

    template<typename ptr_t> 
    struct TData 
    { 
        typedef typename boost::remove_extent<ptr_t>::type value_type; 
        ptr_t data; 
    
        value_type & operator [] ( size_t id ) { return data[id]; } 
        operator ptr_t & () { return data; } 
    }; 
    
    typedef float (&ATYPE) [100][100];
    
    int main( int argc, char ** argv ) 
    { 
        TData<float[100][100]> t;    
    
        t[size_t(1)][size_t(1)] = 5; // note the cast. This works now. No ambiguity as operator[] is preferred over built-in operator
    
        t[1][1] = 5;                 // error, as per the logic given below for Candidate 1 and Candidate 2
    
        // Candidate 1 (CONVERSION rank)
        // User defined conversion from 'TData' to float array
        (t.operator[](1))[1] = 5;
    
        // Candidate 2 (CONVERSION rank)
        // User defined conversion from 'TData' to ATYPE
        (t.operator ATYPE())[1][1] = 6;
    
        return 0; 
    }
    

    编辑:

    这是我的想法:

    对于候选 1(运算符 []),转换序列 S1 是 用户定义的转换——标准转换(int to size_t)

    对于候选2,转换序列S2为 用户定义的转换 -> int 到 ptrdiff_t(对于第一个参数)-> int 到 ptrdiff_t(对于第二个参数)

    转换序列 S1 是 S2 的子集,应该更好。但这里有一个问题......

    以下来自 Standard 的引用应该会有所帮助。

    $13.3.3.2/3 个州 - 标准 转换序列 S1 更好 转换顺序比标准 转换序列 S2 if — S1 是 S2 的正确子序列(比较 中的转换序列 由 13.3.3.1.1 定义的规范形式, 不包括任何左值变换; 身份转换序列是 被认为是任何的子序列 非恒等式转换序列)或, 如果不是那样...

    $13.3.3.2 states-" 用户定义 转换序列 U1 更好 转换序列比另一个 用户定义的转换序列 U2 if 它们包含相同的用户定义 转换函数或构造函数和 如果第二个标准转换 U1的序列优于 第二个标准转换序列 U2。”

    这里的第一部分和条件“如果它们包含相同的用户定义的转换函数或构造函数”不成立。因此,即使 and 条件的第二部分“如果 U1 的第二个标准转换序列优于 U2 的第二个标准转换序列。”成立,S1 和 S2 都不是优先于其他。

    这就是为什么 gcc 的 phantom 错误消息“ISO C++ 说这些是模棱两可的,即使第一个的最差转换优于第二个的最差转换”

    这很好地解释了歧义,恕我直言

    【讨论】:

    • 没有Convert argument 5 to size_tUser defined conversionCandidate 1 意味着什么?
    • 't' 正在使用类成员转换运算符 [] 转换为候选 1 中的浮点数组。我明白关于 5 和 6 没有转换为 size_t 的观点。我会尽快更改帖子
    • TData::operator[]convert t,它返回成员data的子元素。跨度>
    • @David:这不是转换吗? A::operator int(){return m_data;},我们说它将A转换为'int'。
    • @chubsdad: 不是真的,operator type() 是转换运算符并且是特殊的东西,与 operator@ 不同,@ 是非类型。通常转换运算符T1::operator T2 是一个转换,因为给定T1 类型的变量v,它可以在需要T2 类型的上下文中使用,如foo(v)foo 定义为@987654336 @,以某种方式转换元素以匹配T2。请注意,没有呼叫操作员。另一方面,operator[] 是用户必须执行的实际方法调用。它与其他方法没有什么不同(除了调用语法)。
    【解决方案6】:

    重载解决方案令人头疼。但是由于您偶然发现了一个对示例过于具体的修复(消除将索引操作数转换为operator[])(文字是int 类型,但您将使用的大多数变量不是),也许您可​​以概括它:

    template< typename IT>
    typename boost::enable_if< typename boost::is_integral< IT >::type, value_type & >::type
    operator [] ( IT id ) { return data[id]; }
    

    很遗憾,我无法对此进行测试,因为 GCC 4.2.1 和 4.5 在 --pedantic 下毫无怨言地接受了您的示例。这确实引发了一个问题,它是否是编译器错误。

    此外,一旦我消除了 Boost 依赖项,它就通过了 Comeau。

    【讨论】:

    • 他可以使用ptrdiff_t 而不是intsize_t,然后它将适用于任何符合标准的编译器。
    • @Johannes:关键是在没有转换的情况下允许任何一个……你愿意在这个问题上权衡一下吗?
    • 好吧,转换不会受到伤害。如果您使用ptrdiff_t,那么无论用户传递什么参数,在符合标准的编译器上他都不会产生歧义,因为内置运算符也使用ptrdiff_t(索引可以是负数)。这就是为什么我倾向于在我的班级的索引运算符中使用ptrdiff_t
    • @Johannes:您确定 Kirill 收到的错误消息是指多个标准转换路径吗?无论如何,我并没有假装理解规则。我只是根据经验观察,消除标准转换修复了错误。
    • 我在我的答案中添加了一个示例,我认为,如果没有所有运营商规则,这更容易理解。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-01-15
    • 1970-01-01
    • 2015-10-06
    • 2014-10-25
    • 1970-01-01
    • 1970-01-01
    • 2017-03-03
    相关资源
    最近更新 更多