【问题标题】:Conditional operator's return type and two-phase lookup条件运算符的返回类型和两阶段查找
【发布时间】:2016-06-01 08:48:05
【问题描述】:

考虑以下 sn-p:

struct Base { };
struct Derived : Base { };

void f(Base &) { std::cout << "f(Base&)\n"; }

template <class T = int>
void g() {
    Derived d;
    f(T{} ? d : d); // 1
}

void f(Derived &) { std::cout << "f(Derived&)\n"; }

int main() {
    g();
}

在这种情况下,我认为应该在第一阶段查找在// 1 处对f 的函数调用,因为它的参数类型明确为Derived&amp;,因此被解析为f(Base&amp;),即范围内只有一个。

Clang 3.8.0 agrees with me,但GCC 6.1.0 doesn't,并将f 的查找推迟到第二阶段,此时f(Derived&amp;) 被拾取。

哪个编译器是对的?

【问题讨论】:

  • 将此列为核心问题。
  • @Columbo 我倾向于认为规则在这种情况下可以正常工作。如果我在将来某个时候将条件表达式更改为T{} ? d : e,我不希望f 的名称查找突然改变并包含更多候选人。特别是如果e 是一个使用d 初始化的引用,但将来可能会更改为引用其他内容。此外,即使arg 是非依赖的,也能够使用类似f((always_void&lt;T&gt;() , arg)) 的函数调用强制进行依赖名称解析,这对我来说似乎更像是一个特性而不是一个错误。
  • @bogdan 考虑到条件运算符已经有自己的规则来推断其结果的类型,并且条件的类型不会干扰,我看不出有什么令人惊讶的。为了确定表达式是否依赖,可以简单地忽略第一个操作数。另一方面,逗号运算符的结果类型取决于两个操作数,因此这种快捷方式是不可能的,因此您的示例仍然可以正常工作(我同意这是一个非常有用的功能)。
  • @Quentin 啊,所以你认为条件应该被挑出来,因为它不能被重载。这是一个很好的观点。 (一开始我的印象不同,抱歉。)我能想到的一个潜在问题是,使这样的条件表达式不依赖于类型会使 noexcept(T{} ? d : d) 之类的东西不依赖于值,这是不正确的(@ 987654337@ 的构造函数可能会抛出),因此需要添加一些额外的特殊情况。不确定是否值得,但现在我认为值得考虑。我想得越多,你的问题就越有趣!
  • @bogdan 这不是特定于条件运算符的。 noexcept(typeid(T{})) 也不依赖于价值。

标签: c++ language-lawyer compiler-bug name-lookup dependent-name


【解决方案1】:

使用最新版本的C++ standard 目前n4582

在第 14.6 节 (p10) 中,如果名称不依赖于模板参数,则在声明点绑定名称。如果它依赖于模板参数,则在第 14.6.2 节中定义。

第 14.6.2.2 节继续说,如果任何子表达式依赖于类型,则表达式依赖于类型。

现在因为对f() 的调用取决于它的参数。您查看参数类型以查看它是否取决于类型。参数为False&lt;T&gt;::value ? d : d。这里第一个条件取决于T的类型。

因此我们得出结论,调用绑定在实例化点而不是声明点。因此应该绑定到:void f(Derived &amp;) { std::cout &lt;&lt; "f(Derived&amp;)\n"; }

因此 g++ 有更准确的实现。

14.6 名称解析 [temp.res]

第 10 段:

如果名称不依赖于模板参数(定义见 14.6.2),则该名称的声明(或声明集)应在范围内名称出现在模板定义中;该名称绑定到在该点找到的声明(或多个声明),并且此绑定不受在实例化点可见的声明的影响。

14.6.2.2 类型相关表达式 [temp.dep.expr]

除下文所述外,如果任何子表达式都依赖于类型,则表达式是依赖于类型的。

【讨论】:

  • 好吧,这将允许暴露三元运算符重载而不会导致构建中断(因为这样的运算符的存在意味着a?b:c 的类型可能取决于@987654328 的类型@ 实际上,而不是仅仅依赖于标准)。
  • @Yakk:标准没有说类型依赖于a,这显然是由三元运算符本身定义的。它说它是一个 Type-Dependant 表达式(仅用于参数推导),它定义了编译名称的绑定点。
  • 是的。我只是观察到这种与类型的实际依赖不符的正式类型依赖的怪癖意味着如果曾经将三重重载添加到 C++ 中,他们就不必在这里添加类型依赖。例如,一个相当薄弱的论点为什么标准中的这种看似错误或错误可能并不完全愚蠢。
【解决方案2】:

我认为 gcc(顺便说一下,还有 Visual Studio)是正确的。

n4582, §14.6.2.2

除了下面描述的,如果任何子表达式是类型相关的,则表达式是类型相关的。

T{} ? d : d中,有3个子表达式:

  • T{},显然取决于类型
  • d(2 次),不依赖于类型

由于有一个类型相关的子表达式,并且三元运算符没有出现在 §14.6.2.2 的例外列表中,因此它被认为是类型相关的。

【讨论】:

  • 在处理两阶段查找时查看 MSVC 没有多大意义——它根本没有实现它。
【解决方案3】:

根据 c++ 草案 (n4582) §14.7.1.5:

除非函数模板特化已明确 实例化或显式特化的函数模板 当专业化是隐式实例化时 在需要函数定义存在的上下文中引用。 除非调用是对函数模板显式特化或 显式特化类模板的成员函数,a 函数模板或成员函数的默认参数 调用函数时隐式实例化类模板 在需要默认参数值的上下文中。

我会说 gcc 对此更正确。

例如,如果您创建 void g() 的专用版本,则会得到 both compiler doing the same

【讨论】:

    猜你喜欢
    • 2011-07-20
    • 2012-01-22
    • 2013-09-01
    • 2015-09-03
    • 2015-01-29
    • 1970-01-01
    • 2015-07-16
    相关资源
    最近更新 更多