gcc和clang在运算符重载解析期间隐式实例化模板参数。

32

考虑以下代码:

struct A; // incomplete type

template<class T>
struct D { T d; };

template <class T>
struct B { int * p = nullptr; };

int main() {
    B<D<A>> u, v;
    u = v;  // doesn't compile; complain that D<A>::d has incomplete type
    u.operator=(v); // compiles
}
演示。由于u.operator=(v)可以编译,但u = v;不能,因此在后者表达式的重载决议过程中,编译器必须隐式实例化D<A> - 但我不明白为什么需要这个实例化。
为了使事情更有趣,这段代码可以编译:
struct A; // incomplete type

template<class T>
struct D; // undefined

template <class T>
struct B { int * p = nullptr; };

int main() {
    B<D<A>> u, v;
    u = v;
    u.operator=(v);
}

演示.

这里发生了什么?为什么u = v;会导致在第一个例子中隐式实例化D<A> - 一个在B的定义体中根本没有使用过的类型,而在第二个例子中却不会呢?


1
@KugBuBu 这不是相关问题。我在代码中添加了一个p的初始化器,以防万一;但是没有任何改变。(而且VS的模板引擎通常是非标准的,所以...) - T.C.
1
你是否考虑过由于ADL而需要实例化相关类型来查找赋值运算符?根据[basic.lookup.argdep]/2,出现在模板参数中的类被视为相关。 - dyp
1
这个分配并没有太多意义,因为它只能作为成员函数实现。但是我不确定实现者如何解释[over.match.oper]/3中的算法;也许他们总是执行这个查找,即使没有非成员结果。 - dyp
@T.C. 我添加了我的测试,因为VS编译器可以编译不完整类型,这更加令人困惑。(第二个测试有一定的合理性,因为A没有被使用,你不需要知道它的大小) - KugBuBu
显示剩余2条评论
2个回答

20

整个问题的关键在于ADL起作用:

N3797 - [basic.lookup.argdep]

当函数调用(5.2.2)中的后缀表达式是未限定ID时,在通常的未限定查找(3.4.1)期间未考虑的其他命名空间可能会被搜索,并且在这些命名空间中,可以找到命名空间范围的友元函数或函数模板声明(11.3),这些声明在其他情况下不可见。

接下来:

对于函数调用中的每个参数类型T,都有一组零个或多个相关联的命名空间和一组零个或多个相关联的类需要考虑。[...] 命名空间和类的集合是按以下方式确定的:

  • 如果T是类类型[...],则其相关联的类为:...此外,如果T是类模板特化,则其相关联的命名空间和类还包括:提供给模板类型参数的模板参数类型相关联的命名空间和类

D<A>是一个相关联的类,因此在列表中等待轮到它。

现在进入有趣的部分 [temp.inst]/1
除非一个类模板特化已经被显式实例化(14.7.2)或显式专门化(14.7.3),否则,当类模板特化的完整性影响程序语义时,类模板特化将被隐式实例化[...]。
人们可能认为类型D的完整性根本不影响该程序的语义,但是[basic.lookup.argdep]/4说:
在考虑相关联的命名空间时,查找与使用关联命名空间作为限定符(3.4.3.2)执行的查找相同,只是:
[...]
在相关联的类中声明的任何命名空间范围内的友元函数或友元函数模板都可以在其各自的命名空间中看到,即使在普通查找期间它们不可见 (11.3)
也就是说,类类型的完整性实际上会影响友元声明->类类型的完整性因此会影响程序的语义。这也是你的第二个示例有效的原因。
TL;DR D
被实例化。
最后一个有趣的点涉及为什么ADL首先开始。
u = v; // Triggers ADL
u.operator=(v); // Doesn't trigger ADL

§13.3.1.2/2规定除了内置运算符外,不能有非成员operator=。将此加入到[over.match.oper]/2中:
“非成员候选集是按照通常的未限定函数调用名称查找规则(3.4.2)在表达式上下文中进行operator@的未限定查找结果,但忽略所有成员函数。”
逻辑结论是:如果表11中没有非成员形式,则执行ADL查找没有意义。然而[temp.inst]p7放宽了这一点:
“如果重载决议过程可以确定正确的调用函数而不实例化类模板定义,则不确定是否实际进行该实例化。”
这就是为什么clang首先触发整个“ADL->隐式实例化”过程的原因。

r218330 开始(在撰写本文时,已提交几分钟),这种行为已经更改,不再执行 ADL 以进行 operator=


参考文献

感谢 Richard Smith 和 David Blaikie 帮助我解决了这个问题。


看起来有两个不同的问题 - 1)编译器是否需要/允许为operator =实例化D<A>?(我认为我们都同意ADL要求实例化可以是非成员的运算符。)2)在需要实例化D<A>的情况下(例如二进制operator-),为什么第二个示例会编译?隐式实例化已声明但未定义的模板应使程序不合法([temp.inst]/p8)。 - T.C.
Clang肯定是在r218330之前的版本。至于2)它的完整性不会影响程序的语义。 - Marco A.
它确实适用于该定义,但这里的重点是是否应该对此代码执行ADL。 - Marco A.
所有都正确(http://coliru.stacked-crooked.com/a/37a510ca0ccd112b),虽然在这种情况下没有错误,它是一种明确定义的行为。 - Marco A.
2
啊哈,所以我走对了路。感谢您比我更有耐心,并与clang开发人员一起检查这个问题。 - dyp
显示剩余2条评论

-2

我认为在Visual Studio 2013中,代码应该是这样的(不需要使用= nullptr):

  struct A; // incomplete type

  template<class T>
  struct D { T d; };

  template <class T>
  struct B { int * p; };

  int void_main() {
    B<D<A>> u, v;
    u = v;          //  compiles
    u.operator=(v); // compiles
    return 0;
    }

在这种情况下,它应该能够编译成功,因为不完整的类型可以用于特定模板类专业化使用。
至于运行时错误-变量v在未初始化的情况下使用-是正确的-结构体B没有任何构造函数=> B :: p未初始化,可能包含垃圾。

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