在switch语句的条件中同时具有模板和非模板转换运算符的类

26

这个问题最初出现在这个问题中。考虑以下代码:

class Var
{
public:

    operator int () const
    { return 0; }

    template <typename T>
    operator T () const
    { return T(); }

};

int main()
{
    Var v;
    switch (v)
    { }
}

没有 operator int() const { return 0; },g++和clang都会拒绝该代码。
然而,加上operator int()的上面代码被clang接受,但被g++拒绝,并显示以下错误:
main.cpp:17:14: error: default type conversion can't deduce template argument for 'template<class T> Var::operator T() const'
     switch (v)
              ^

哪个编译器是正确的?


1
Switch语句可以针对多种类型进行切换,不仅限于int - Some programmer dude
3
从标准的措辞来看,似乎不应该进行重载决策,并且确实只有一个可能的函数可以执行转换。这将使g ++正确。但是,我不确定。 - Brian Bi
1
жҲ‘жӣҙж–°дәҶжҲ‘зҡ„зӯ”жЎҲпјҢжҲ‘еҸ‘зҺ°N3323ж¶өзӣ–дәҶдёҠдёӢж–ҮиҪ¬жҚўе‘Ёеӣҙзҡ„йҖ»иҫ‘пјҢ并且е®ғејәзғҲжҢҮеҮәclangеңЁиҝҷж–№йқўжҳҜжӯЈзЎ®зҡ„гҖӮ - Shafik Yaghmour
1
@T.C.,ShafikYaghmour,如果我们看一下三个编译器clang++、g++和msvc++,它们各自的“C++11”模式(或者说msvc++的默认模式)包含了标准中提出的一些缺陷的解决方案。如果我们把C++11仅视为已发布的标准,那么N3485就不是“C++11草案”。如果我们把C++11看作是旨在实现经过修复但没有增补,我认为N3485可以用来谈论C++11。无论如何,C++11有多种定义。 - dyp
2
我为此提交了一个gcc错误报告,抄送@dyp - Shafik Yaghmour
显示剩余12条评论
6个回答

17
我认为这里应该使用clang
我们可以从draft C++ standard的第6.4.2switch语句中看到,这涉及到上下文隐式转换。第2段说(*接下来是我的强调):

条件应该是整数类型、枚举类型或类类型。如果是类类型,则条件会被上下文隐式转换(第4条)为整数或枚举类型。

我们可以看到我们需要使用的部分是4标准转换,而第5段涵盖了这些情况,它说:
某些语言结构需要转换为具有指定类型集合之一的值。在这种上下文中出现的类类型E的表达式e被称为上下文隐式转换为指定类型T,仅当e可以隐式转换为以下确定的类型T时才是良好形式:搜索E的转换函数,其返回类型为cv T或引用cv T,使得T由上下文允许。应该确切地有一个这样的T。 此内容不涉及第8.5节,该节通过明确引用第13.3节允许重载解析,而不允许重载解析我们无法使用:
template <typename T>
operator T () const

因此,没有歧义。
请注意,这与第4段不同,第4段涵盖了在“if”、“while”等上下文中的布尔转换,并且说(我强调):
某些语言结构要求将表达式转换为布尔值。出现在这种上下文中的表达式e被认为是在上下文中转换为bool,并且仅当声明bool t(e) 的发明临时变量t(8.5)的形式良好时,它才是形式良好的。
这特别允许重载分辨率并直接引用覆盖此内容的第13.3节。允许它是有道理的,因为我们有一个特定的目标类型bool可转换到其中,在switch案例中我们没有。
为什么?
我们可以通过查看N3323: A Proposal to Tweak Certain C++ Contextual Conversions, v3来解决这个问题。引用整篇论文可能有些困难,因此我将尝试引用足够的上下文。它说:
在C ++表达式出现的上下文经常影响表达式的评估,因此可能会对表达式施加要求,以确保这种评估是可能的。[...]
在四种情况下,FDIS(N3290)使用不同的语言来指定类似的上下文相关转换。在这四个上下文中,当操作数为类类型时,该类型必须具有“单个非显式转换函数”以适合的(上下文特定)类型。[...]
并包括:
[stmt.switch] / 2:“条件应为整数类型,枚举类型或具有单个非显式转换函数以整数或枚举类型存在的类类型(12.3)。”
并说:
在引言中提到的这四个情境中,主要问题似乎在于它们共同具有的有用但非常严格的要求,即将类限制为仅有一个转换运算符。另一个问题是当前措辞中“单一”限定符的范围。在类中必须只有一个转换函数,还是可以有多个转换函数,只要其中一个适合上下文即可?当前语言在这一点上似乎不太清楚。也不清楚生成适当类型引用的转换运算符是否是适当的转换运算符。(关于这一点的问题已于2011年2月21日发布到核心反射器上,但截至本文写作时未得到回答。)目前编译器实践似乎允许这样的运算符,但当前语言似乎不允许。为了解决所有这些问题,我们建议改用由[conv]/3定义的术语“上下文转换为bool”的经过验证的方法。因此,我们建议对[conv]/3进行适度补充,以定义上下文转换为其他指定类型,然后引用这个新定义。新的措辞如下:
某些其他语言结构需要类似的转换,但要将其转换为一个特定类型的值,该类型适用于该结构。在这种情况下出现的类类型E的表达式e被称为在上下文中隐含地转换为指定类型T,并且仅当e可以被隐含地转换为以下确定的类型T时才是良好的:在E中搜索返回类型为cv T或引用cv T的转换函数,使得T由上下文允许。应当恰好有一个这样的T。
注意N3486: C++ Editor's Report, October 2012告诉我们N3323何时被纳入草案标准。
更新
提交了一个gcc bug report

9

6.4.2/2 switch语句(重点在于“single non-explicit conversion function”)

条件应为整型、枚举类型或类类型,对于该类类型必须存在一个非显式转换函数(12.3),可将其转换为整型或枚举类型。如果条件是类类型,则通过调用该转换函数将其转换,并使用转换结果代替原始条件,在本节的其余部分中使用。

因此,我的解释是g++是正确的。


但问题在于,gcc没有将非模板转换运算符视为比模板版本更好的匹配项,而clang则是如此。您的答案没有解决这一部分。 - Praetorian
1
@Praetorian 标准规定:“单个非显式转换函数”。但并未说明重载解析。 - Brian Bi
1
另一方面,可以提出一个合理的论点,即模板转换函数不算作“到整数或枚举类型”,特别是因为无法将T推导为任何整数或枚举类型,在这种情况下只有一个符合条件的转换函数。 - user743382
4
我给C++标准讨论邮件列表发了电子邮件。让我们看看他们的想法 :-) - Brian Bi
1
模板是否隐式转换为整数类型?当然:int x = v; char c = v; ...。事实上,类型无法推断并不意味着它不是一种转换。请注意,引用并未说“在switch语句的上下文中”或类似的内容,它只关注其存在性。 - David Rodríguez - dribeas
显示剩余5条评论

3
我相信gcc是正确的,但标准存在缺陷。
gcc是正确的,因为标准规定了对于在switch语句中使用的类型,只应有一个非显式转换运算符用于整型或枚举类型。
标准是错误的,因为检测case是否成立需要解决停机问题。
operator T可以附加任意复杂度的SFINAE子句。根据标准,编译器必须确定是否存在一个T,该T是一个枚举类型。
template<class...Ts>
struct evil {
  enum { bob = 3+sizeof...(Ts) };
};

struct test {
  operator int() const { return -1; };
  template<class T, typename std::enable_if<T::bob==2>::type* unused=nullptr>
  operator T() const { return T::bob; }
};
int main() {
  switch( test{} ) {
    case -1: std::cout << "int\n"; break;
    case 2: std::cout << "bob\n"; break;
    default: std::cout << "unexpected\n"; break;
  }
}

上面的代码演示了一个情况,我们有无限数量的“enum”隐式可用。我们有一个“operator T”,如果且仅当“T :: bob == 2”时会转换为类型“T”。现在,在我们的程序中没有这样的“enum”(即使我们删除了“3 +”也不会有,因为它不是“enum class” - 可以轻松纠正)。
因此,“test”只能转换为“int”,因此“switch”语句应该编译。gcc未通过此测试,并声称“template operator T”使其模棱两可(自然地没有告诉我们“T”是什么)。
将“enum type”替换为“enum class type”,并删除“3 +”,则根据标准,该“switch”语句是非法的。但是,为了找出这一点,编译器基本上必须实例化程序中的所有可能的模板,寻找具有该属性的秘密“enum”。经过一些工作,我可以迫使编译器解决NP完全问题(或者,排除编译器限制,停机问题),以确定程序是否应该编译。
我不知道正确的措辞应该是什么。但是现有的措辞肯定不是可靠的。

operator T()如何附加SFINAE子句并仍然可用? - aschepler
@aschepler template<class T,class=std::enable_if_t<T::bob==2>> operator T() - Yakk - Adam Nevraumont
@aschepler 嗯。不可达的 operator long<long,void>() 可能会破坏我的计划:它存在,而 SFINAE 无法阻止其存在。因此,这可能只有在概念轻量级到来时才会出现。嗯。 - Yakk - Adam Nevraumont
@dyp 更新了一个使用你的技巧的具体示例。并展示了gcc中的一个缺陷(好吧,是在要求的不可能性方面)。 - Yakk - Adam Nevraumont
1
@T.C. 呵呵。这也许就是为什么 Shafik 是对的,clang 也可能是对的原因。 - Yakk - Adam Nevraumont
显示剩余4条评论

2
在我看来,基于“§13.3.3/1最佳可行函数[over.match.best]”,非模板重载转换运算符(即operator int() const)在重载决议选择方面具有更高的优先级,而其模板版本(即template <typename T> operator T () const)则次之。
因此,在重载决议中,正确选择operator int() const而不是template <typename T> operator T () const,因为它是最佳可行函数。
此外,由于非模板版本将被选择而不是模板版本(即编译器不会实例化/限定模板),因此class Var将只有一个转换函数,从而满足“§6.4.2/2 switch语句[stmt.switch]”中对单一整数转换的要求。
因此,Clang是正确的,而GCC是错误的。

3
我认为非模板函数与通过模板实例化选择的等效签名之间的过载决议优先级在这里并不相关。 - David Rodríguez - dribeas

2
以下是相关引用,但最终答案在很大程度上取决于解释。 我现在甚至无法决定我的最爱。
N3797 6.4.2 / 2:
条件应为整数类型、枚举类型或类类型。 如果是类类型,则将上下文中的条件隐式转换(第4条款)为整数或枚举类型。
4/5:
某些语言结构需要转换为具有适合该结构的一组指定类型之一的值。 在此类上下文中出现的类类型E的表达式e被称为上下文隐式转换为指定类型T,并且当且仅当e可以被隐式转换为由以下方式确定的类型T时才是良好的:搜索E以查找返回类型为cv T或指向cv T的引用的转换函数,使得T被上下文允许。 必须恰好有一个这样的T。
14.5.2 / 6:
转换函数的特化不通过名称查找找到。 相反,在使用的上下文中考虑任何可见的转换函数模板。 对于每个这样的运算符,如果参数推导成功(14.8.2.3),则使用所得到的特化,就像通过名称查找找到一样。
14.5.2 / 8:
重载分辨率(13.3.3.2)和部分排序(14.5.6.2)用于在转换函数模板和/或非模板转换函数的多个特化中选择最佳转换函数。
解释1:4/5说“转换函数”,而不是“转换函数和转换函数模板”。 因此,Var :: operator int()const是唯一的选择,clang是正确的。
解释2 [弱?]:14.5.2要求我们通过重载分辨率和部分排序比较转换函数模板,其初始状态与非模板转换函数相同。 这些比较函数模板专业化和函数,而不是函数模板,因此我们将执行模板参数推导。 转换函数模板的模板参数推导需要目标类型。 尽管我们通常有一个更清晰的目标类型,但在这种情况下,我们将尝试(理论上)允许类型集中的所有类型。 但是,非模板函数明显是比所有模板专业化更好的可行函数,因此重载分辨率选择非模板函数。 clang是正确的。
解释3:由于重载分辨需要模板参数推导,而模板参数推导需要已知的目标类型,因此必须首先考虑4/5的语义,然后可以使用它的转换类型(如果有的话)来进行重载分辨过程。14.5.2要求考虑转换函数模板,但是我们发现有多个有效类型 T,其中我们有一个到T的转换函数[该函数可能是函数模板特化]。程序不合法,因此g++是正确的。

1
如果我正确理解了这个关于重载的部分,那么Clang是正确的。
13.3.3最佳可行函数[over.match.best]
[...]鉴于这些定义,可以将一个可行的函数F1定义为比另一个可行的函数F2更好的函数,如果对于所有参数i,ICSi(F1)不是比ICSi(F2)更差的转换序列,然后[...] - F1是非模板函数且F2是函数模板特化,或者如果不是那样,[...]
草案可以免费阅读。不确定13.3.3中是否有任何更改被放入最终规范中(我没有付费)。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

我会提交一个G++的错误 :-) 他们可能会用不同的标准部分来回复,但它似乎不符合标准。

针对aschepler的评论进行编辑:

来自: http://publib.boulder.ibm.com/infocenter/comphelp/v101v121/index.jsp?topic=/com.ibm.xlcpp101.aix.doc/language_ref/cplr315.html

假设f是一个重载函数名。当你调用重载函数f()时,编译器会创建一组候选函数。这组函数包括所有可以从调用f()的点访问的名为f的函数。编译器可以将可访问的函数f的另一种表达形式作为候选函数之一,以便于解决重载问题。

在创建一组候选函数之后,编译器创建一组可行函数。这组函数是候选函数的子集。每个可行函数的参数数量与您用于调用f()的参数数量相同。


我认为模板转换一开始就不可行。 - aschepler
"Viable" 在重载匹配的上下文中有特定的含义。 - Caladain
是的,它确实如此。根据13.3.1/7:“在每种情况下,如果候选函数是函数模板,则使用模板参数推导生成候选函数模板特化。然后按照通常的方式处理这些候选函数。”但是在这里,模板参数推导将失败,因此没有特化程序可以在候选集中,而可行函数集是候选集的子集。 - aschepler
至少,我会在九月的CPPcon上询问,并在那之前在这里发布,除非他们在那之前回复邮件列表 :-) - Caladain

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