【问题标题】:Lookup of dependent names in C++ template instantiation在 C++ 模板实例化中查找依赖名称
【发布时间】:2015-01-26 12:17:51
【问题描述】:

当我尝试编译这段代码时

// void foobar(int); 

template <class T>
struct Foo {
  void bar(T t) { foobar(t); };
};

void foobar(int);

template class Foo<int>;

使用 g++ 4.8.2 我收到以下错误消息

foo.cc: In instantiation of ‘void Foo<T>::bar(T) [with T = int]’:
foo.cc:10:16:   required from here
foo.cc:5:27: error: ‘foobar’ was not declared in this scope, and no 
             declarations were found by argument-dependent lookup at 
               the point of instantiation [-fpermissive]
   void bar(T t) { foobar(t); };
                           ^
foo.cc:8:6: note: ‘void foobar(int)’ declared here, later in the translation unit
 void foobar(int);
      ^

(使用 clang 3.4 几乎相同)。

首先,我认为代码是正确的并且应该编译,因为 foobar 是模板声明中的一个依赖名称,并且应该只在模板实例化的第二阶段查找。当这在最后一行完成时,'foobar(int)' 已经被声明了。顺便说一句,当我取消最上面一行的注释时,代码会编译,但两个声明都在实例化之前,所以这无关紧要。

其次,错误信息本身对我来说似乎是矛盾的。它说“在实例化时没有找到声明”,即 foo.cc:10:16,它说它是在 foo.cc:8:6 处“稍后”声明的。对于我对数字和英语的所有了解,我将其称为“之前”而不是“之后”。

那么,这是 gcc 中的错误还是我做错了什么?由于这在我看来是一种常见的使用模式,但我不太相信。

顺便说一句:当我在 MSDN (http://msdn.microsoft.com/en-us/library/dx2zs2ee.aspx) 上使用 g++ 尝试“依赖类型的名称解析”的第二个示例时,结果与 vc++ 不同,后者(通常不是,但在这种特定情况下)会破坏这一点是 g++ 中的一个错误。

【问题讨论】:

  • 好问题,我还有另一个问题,在某种意义上与你的相反:stackoverflow.com/questions/25188353/…
  • 此页面未将函数调用列为有效的依赖类型或依赖表达式:en.cppreference.com/w/cpp/language/dependent_name
  • @didierc foobar 在这里是一个依赖名称,因为它是一个使用具有依赖类型的参数调用的函数(基本原理:在类型确定之前无法完成函数的重载解析)
  • 在 SO stackoverflow.com/questions/7369314/… 上找到了以下问题,根据接受的答案 class A{};void foobar(A);template class Foo&lt;A&gt;; 应该编译,并且确实如此。似乎名称查找是不同的,因为搜索 A 的关联名称空间,但 int 没有关联名称空间作为基本类型。这有意义吗?
  • @kevinarpe 你的意思是在错误输出中?它们来自 GCC,是 unicode 字符 \u2018 和 \u2019。相反,clang 使用普通的单引号 \u27。在 bash 中尝试 echo $'\x27\u2018\u2019' 以查看它们。

标签: c++ templates language-lawyer argument-dependent-lookup


【解决方案1】:

tl;dr Foo&lt;int&gt; 不调用任何 ADL,但 Foo&lt;X&gt; 会(其中 X 是类类型)。


首先,在这段代码中,foobar 是一个依赖名称,因为 (C++14/N3936) [temp.dep]/1

在一个表达式中:

postfix-expression ( expression-list opt )

如果后缀表达式是非限定 ID,则非限定 ID 表示从属名称,如果 [...]

  • 表达式列表中的任何表达式都是依赖于类型的表达式 (14.6.2.2),或者

t 是一个依赖名称,因为它是声明T t 的一部分,其中T 是一个模板参数,因此是一个依赖类型。

转到依赖名称解析,[temp.dep.res]/1 介绍了这样一个事实,即名称既可以在定义上下文中查找,也可以在实例化上下文中查找,并定义了实例化上下文的位置。为简洁起见,我省略了它,但在此示例中,template class Foo&lt;int&gt;; 是实例化点。

下一位是[temp.dep.candidate]/1:

对于 后缀表达式 是依赖名称的函数调用,使用通常的查找规则(3.4.1、3.4.2)找到候选函数,除了:

  • 对于使用非限定名称查找 (3.4.1) 的查找部分,只能找到来自模板定义上下文的函数声明。
  • 对于使用关联命名空间 (3.4.2) 的查找部分,只能找到在模板定义上下文或模板实例化上下文中找到的函数声明。

最后两部分是两阶段查找的“两个阶段”。 (注意——本节的措辞从 C++11 更改为 C++14,但效果相同)。

在第一阶段 3.4.1 中,没有找到 foobar 的名称。


所以我们进入第二阶段。查找名称的实际地点如 3.4.2 中所述。文字很长,但这里有两条相关规则:

  • 如果 T 是基本类型,则其关联的命名空间和类集都是空的。

  • 如果 T 是类类型(包括联合),则其关联的类是:类本身;它所属的类别(如有的话);及其直接和间接基类。其关联的命名空间是其关联类的最内层封闭的命名空间。 [...]

因此,当您实例化 Foo&lt;int&gt; 时,查找的第二阶段不会引入任何额外的命名空间进行搜索。

但是,如果您将示例更改为具有struct X {};,然后将int 更改为X,则代码编译。这是因为后一个要点:类类型参数的 ADL 会搜索该类的封闭命名空间(现在是全局命名空间),但是内置类型参数的 ADL 不会搜索全局命名空间。

【讨论】:

  • ADL 被调用。 ADL 只是不考虑单个命名空间。
  • 感谢您的冗长回答,马特。因此,根据 [basic.lookup.argdep]/2 int 没有关联的命名空间和类,因此在 [temp.dep.candidate]/1 中,只有“不合格名称查找”部分起作用,它只看模板定义上下文。是否有任何缺点,还像在项目符号点 2 中那样查看模板实例化上下文?
  • @ElmarZander 如果您问为什么在实例化时无法再次完全查找依赖名称...我不知道,但我敢肯定有人写过编译器可能有话要说:)。最初 C++ 只有第一阶段;然后添加了 ADL,这样cout &lt;&lt; x; 之类的东西就可以工作了。也许从那时起他们只是不想再惹它了。
  • 实际上,我在 gcc 中他们确实这样做了很长时间,请参阅 gcc.gnu.org/bugzilla/show_bug.cgi?id=29131。他们只是随着 gcc 4.7 (gcc.gnu.org/gcc-4.7/porting_to.html) 的发布对其进行了更改,当我查看错误历史时,在我看来,似乎没有人对此有真正的问题,只是不太符合。他们甚至在文档gcc.gnu.org/onlinedocs/gcc/Name-lookup.html 中这样描述它(现在可能应该重写)。无论如何,我必须使我的代码适应它,这会使它在某些地方变得更加混乱。
  • 这是不正确的,句号。这里没有 NDR 或 UB。 [temp.dep.candidate] 说,如果“调用格式不正确,或者如果在关联命名空间中的查找考虑到在所有翻译单元中的这些命名空间中引入的具有外部链接的所有函数声明,调用将找到更好的匹配,则发生 UB,不仅仅是考虑在模板定义和模板实例化上下文中找到的那些声明。”根本没有关联的命名空间。
【解决方案2】:

第二阶段查找仅包括不能在第一阶段应用的名称查找规则,例如 ADL。两阶段查找就是这样——有些名称在第一阶段查找,有些名称在第二阶段查找。这种特殊的名称是第一阶段的名称,因为编译器完全能够在第一阶段在函数的命名空间中查找foobar

Visual C++ 不实现两阶段名称查找。

【讨论】:

  • 那么,根据标准,这是来自 gcc 4.8.2 的正确行为吗?顺便说一句:我现在还检查了 gcc 4.4 和 4.6,并且两者都可以编译。它们(和 vc++)是否不符合标准?
  • 还有一个问题:当模板参数尚未指定时,编译器如何能够在第一阶段查找 foobar? (或者我把第一/第二阶段的事情弄错了吗?)
  • @MattMcNabb 我试过了,但我的 g++ 仍然抱怨。现在实例化来自f.bar(5) 行,但其他错误是一样的。
  • @ElmarZander 我的意思是编译器抱怨是正确的
  • @Puppy +1,这是问题的核心
【解决方案3】:

在我看来是正确的。虽然重载决议只在第 2 阶段完成,但在第 1 阶段您已经知道foobar(t) 是一个函数调用表达式。如果foobar 命名一个类型,t 甚至都不会是从属名称。

【讨论】:

  • 抱歉,您认为什么是正确的?代码还是编译器错误?
猜你喜欢
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-13
  • 1970-01-01
  • 2011-08-12
  • 1970-01-01
相关资源
最近更新 更多