Skip to content

Latest commit

 

History

History
617 lines (443 loc) · 14.3 KB

06_name_lookup.md

File metadata and controls

617 lines (443 loc) · 14.3 KB
namespace jc {

struct A {};
struct B {};
void f1(int) {}
void f2(A) {}

}  // namespace jc

namespace jd {

void f1(int i) {
  f1(i);  // 调用 jd::f1(),造成无限递归
}

void f2(jc::A t) {
  f2(t);  // 通过 t 的类型 jc::A 看到 jc,通过 jc 看到 jc::f2()
          // 因为 jd::f2() 也可见,此处产生二义性调用错误
}

void f3(jc::B t) {
  f3(t);  // 通过 t 的类型 jc::B 看到 jc,但 jc 中无 jc::f3()
          // 此处调用 jd::f3(),造成无限递归
}

}  // namespace jd

int main() {}
  • Qualified name lookup 即对使用了作用域运算符的名称做查找,查找在受限的作用域内进行
namespace jc {

int x;

struct Base {
  int i;
};

struct Derived : Base {};

void f(Derived* p) {
  p->i = 0;        // 找到 Base::i
  Derived::x = 0;  // 错误:在受限作用域中找不到 ::x
}

}  // namespace jc

int main() {}
  • Unqualified name lookup 即对不指定作用域的名称做查找,先查找当前作用域,若找不到再继续查找外围作用域
namespace jc {

extern int x;  // 1

int f(int x) {  // 2
  if (x < 0) {
    int x = 1;  // 3
    f(x);       // 使用 3
  }
  return x + ::x;  // 分别使用 2、1
}

}  // namespace jc

int main() {}
  • ADL 即实参依赖查找,对于一个类,其成员函数与使用了它的非成员函数,都是该类的逻辑组成部分,如果函数接受一个类作为参数,编译器查找函数名时,不仅会查找局部作用域,还会查找类所在的命名空间
#include <iostream>
#include <string>

namespace jc {

struct A {};

void f(const A&) {}  // f() 是 A 的逻辑组成部分

}  // namespace jc

jc::A a;

int main() {
  f(a);  // 通过 ADL 找到 jc::f(),如果没有 ADL,就要写成 jc::f(a)
  std::string s;
  std::cout << s;  // std::operator<<() 是 std::string 的逻辑组成部分
  // 如果没有 ADL,就要写成 std::operator<<(std::cout, s)
}
  • ADL 是通过实参查找,对于函数模板,查找前无法先得知其为函数,也就无法确定实参,因此不会使用 ADL
namespace jc {

class A {};

template <typename>
void f(A*) {}

}  // namespace jc

void g(jc::A* p) {
  f<int>(p);  // 错误,不知道 f<int> 是函数,所以不知道 p 是实参,不会用 ADL
}

int main() {}
  • ADL 会忽略 using 声明
namespace jc {

template <typename T>
constexpr int f(T) {
  return 1;
}

}  // namespace jc

namespace jd {

using namespace jc;  // 忽略 using 声明,不会调用 jc::f

enum Color { red };
constexpr int f(Color) { return 2; }

}  // namespace jd

constexpr int f(int) { return 3; }

static_assert(::f(jd::red) == 3);  // 受限的函数名称,不使用 ADL
static_assert(f(jd::red) == 2);    // 使用 ADL 找到 jd::f()

int main() {}
  • ADL 会查找实参关联的命名空间和类,关联的命名空间和类组成的集合定义如下
    • 内置类型:集合为空
    • 指针和数组类型:所引用类型关联的命名空间和类
    • 枚举类型:关联枚举声明所在的命名空间
    • 类成员:关联成员所在的类
    • 类类型:关联的类包括该类本身、外围类型、直接和间接基类,关联的命名空间为每个关联类所在的命名空间,如果类是一个类模板实例则还包含模板实参本身类型、模板的模板实参所在的类和命名空间
    • 函数类型:所有参数和返回类型关联的命名空间和类
    • 类成员指针类型:成员和类关联的命名空间和类
  • 友元声明在外围作用域不可见,因为如果可见的话,实例化类模板会使普通函数的声明可见,如果没有先实例化类就调用函数,将导致编译错误,但如果友元函数所在类属于 ADL 的关联类集合,则在外围作用域可以找到该友元声明,且调用时,未实例化的类会被实例化
namespace jc {

template <typename T>
class A {
  friend void f() {}
  friend void f(A<T>) {}
};

void g(const A<int>& a) {
  f();   // f() 无参数,不能使用 ADL,不可见
  f(a);  // f(A<int>) 关联类 A<int> 所以可见,若类 A<int> 未实例化则调用时实例化
}

}  // namespace jc

int main() {}
  • 为了便于查找,在类作用域中,类名称是自身类型的 public 别名,该名称称为注入类名
namespace jc {

int A;

struct A {
  void f() {
    A* p;    // OK:A 是注入类名
    ::A* q;  // 错误:查找到变量名 A,隐藏了 struct A 的名称
  }
};

}  // namespace jc

int main() {}
  • 类模板的注入类名可以被用作模板名或类型名
namespace jc {

template <template <typename> class>
struct A {};

template <typename T>
struct B {
  B* a;            // B 被当作类型名,等价于 B<T>
  B<void>* b;      // B 被当作模板名
  using c = A<B>;  // B 被当作模板名
  A<jc::B> d;      // jc::B 不是注入类名,总会被当作模板名
};

}  // namespace jc

int main() {}

非模板中的上下文相关性

  • 解析理论主要面向上下文无关语言,而 C++ 是上下文相关语言,为了解决这个问题,编译器使用一张符号表结合扫描器和解析器
  • 解析某个声明时会把它添加到表中,扫描器找到一个标识符时,会在符号表中查找,如果发现该符号是一个类型就会注释这个标记,如编译器看见 x*,扫描器会查找 x,如果发现 x 是一个类型,解析器会看到标记如下,认为表达式是一个声明
identifier, type, x
symbol, *
  • 如果 x 不是类型,则解析器从扫描器获得标记如下,表达式被视为一个乘积
identifier, nontype, x
symbol, *
  • 对于 A<1>(0),如果 A 是类模板,则表达式是把 0 转换成 A<1> 类型。如果不是类模板,表达式等价于 (A<1)>0,计算表达式 A 小于 1 的结果,再将结果与 0 比较大小。因此解析器先查找 < 前的名称,如果名称是模板才会把 < 看作左尖括号,其他情况则看作小于号
namespace jc {

template <bool B>
struct A {
  static const bool value = B;
};

static_assert(A<(1 > 0)>::value);  // 必须使用小括号

}  // namespace jc

int main() {}

当前实例化(current instantiation)和未知特化(unknown specialization)

  • Name lookup 对 dependent name 与 non-dependent name 有不同的查找规则,在模板定义中,依赖于模板参数的名称称为 dependent name,dependent name 包含当前实例化和未知特化。类模板的注入类名属于当前实例化,依赖于模板参数但不是当前实例化的为未知特化(unknown specialization)
namespace jc {

template <typename T>
struct A {
  using type = T;

  A* a;        // A 是当前实例化
  A<type>* b;  // A<type> 是当前实例化
  A<T*>* c;    // A<T*> 是未知特化

  struct B {
    A* a;        // A 是当前实例化
    A<type>* b;  // A<type> 是当前实例化
    B* c;        // B 是当前实例化
  };

  struct C {
    A* a;        // A 是当前实例化
    A<type>* b;  // A<type> 是当前实例化
    B* c;        // 不在 B 的作用域内,B 是未知特化
    C* d;        // C 是当前实例化
  };
};

template <>
struct A<int>::B {
  int i;
};

}  // namespace jc

int main() {
  jc::A<double>::C{}.c->a;
  jc::A<int>::C{}.c->i;  // 使用特化的 A<int>::B
}

typename 消歧义符

  • 模板名称的问题主要是不能有效确定名称,模板中不能引用其他模板的名称,因为其他模板可能有使原名称失效的特化
namespace jc {

template <typename T>
struct A {
  static constexpr int x = 0;  // x 是值
};

template <typename T>
struct B {
  int y;

  void f() {
    A<T>::x* y;  // 默认被看作乘法表达式
  }
};

template <>
struct A<int> {
  using x = int;  // x 是类型
};

}  // namespace jc

int main() {
  jc::B<int>{}.f();   // A<int>::x* y 是声明,int* y
  jc::B<void>{}.f();  // A<void>::x* y 是乘法,0 * y
}
  • Dependent name 默认不会被看作类型,如果要表明是类型则需要加上 typename 消歧义符
namespace jc {

template <typename T>
struct A {
  static constexpr int x = 0;  // x 是值
};

template <typename T>
struct B {
  int y;

  void f() {
    typename A<T>::x* y;  // 默认被看作声明
  }
};

template <>
struct A<int> {
  using x = int;  // x 是类型
};

}  // namespace jc

int main() {
  jc::B<int>{}.f();   // A<int>::x* y 是声明,int* y
  jc::B<void>{}.f();  // A<void>::x* y 是乘法,0 * y
}
  • typename 消歧义符只能用于不在基类列表和初始化列表中的 dependent name,用作用域运算符访问 dependent name 中的成员类型时,必须指定 typename 消歧义符
namespace jc {

struct Base {
  int i;
};

template <typename T>
struct A {
  using type = T;
};

template <typename T>
struct Derived : A<T>::type {  // 基类列表中不能加 typename 消歧义符
  Derived()
      : A<T>::type  // 初始化列表中不能加 typename 消歧义符
        (typename A<T>::type{0})  // 必须加 typename 消歧义符
  {}

  A<T> f() {
    typename A<T>::type* p;  // 必须加 typename 消歧义符
    return {};
  }

  A<int>::type* s;  // non-dependent name,typename 消歧义符可有可无
};

}  // namespace jc

int main() { jc::Derived<jc::Base>{}.f(); }

template 消歧义符

  • 访问模板参数的 dependent name 时,要在 dependent name 前加 template 消歧义符,才能让编译器知道引用的是一个模板,否则 < 会被视为小于号
namespace jc {

template <typename T>
struct A {
  template <typename U>
  struct Impl {
    template <typename Y>
    static void f() {}
  };

  template <typename U>
  static void f() {}
};

}  // namespace jc

template <typename T>
void test() {
  T::template Impl<T>::template f<T>();
  T::template f<T>();
}

int main() { test<jc::A<int>>(); }

Non-dependent base

  • Non-dependent base 是不用知道模板实参就可以推断类型的基类,派生类中查找 non-dependent name 时会先查找 non-dependent base,再查找模板参数列表
#include <type_traits>

namespace jc {

template <typename>
struct Base {
  using T = char;
};

template <typename T>
struct Derived1 : Base<void> {  // non-dependent base
  using type = T;               // T 是 Base<void>::T
};

template <typename T>
struct Derived2 : Base<T> {  // dependent base
  using type = T;            // T 是模板参数
};

static_assert(std::is_same_v<Derived1<int>::type, char>);
static_assert(std::is_same_v<Derived2<int>::type, int>);

}  // namespace jc

int main() {}

Dependent base

  • 对于 non-dependent name,不会在 dependent base 中做查找
namespace jc {

template <typename>
struct Base {
  static constexpr int value = 1;
};

template <typename T>
struct Derived : Base<T> {  // dependent base
  constexpr int get_value() const {
    return value;  // 错误:不会在 dependent base 中查找 non-dependent name
  }
};

}  // namespace jc

int main() {}
  • 如果要在 dependent base 中查找,则可以使用 this-> 或作用域运算符将 non-dependent name 变为 dependent name
namespace jc {

template <typename>
struct Base {
  static constexpr int value = 1;
};

template <typename T>
struct Derived : Base<T> {  // dependent base
  constexpr int get_value() const {
    return this->value;  // dependent name,会在 dependent base 中查找
  }
};

template <>
struct Base<bool> {
  static constexpr int value = 2;
};

}  // namespace jc

int main() {
  static_assert(jc::Derived<int>{}.get_value() == 1);
  static_assert(jc::Derived<bool>{}.get_value() == 2);
}
  • 或者使用 using 声明,这样只需要引入一次
namespace jc {

template <typename>
struct Base {
  static constexpr int value = 1;
};

template <typename T>
struct Derived : Base<T> {  // dependent base
  using Base<T>::value;

  constexpr int get_value() const {
    return value;  // dependent name,会在 dependent base 中查找
  }
};

template <>
struct Base<bool> {
  static constexpr int value = 2;
};

}  // namespace jc

int main() {
  static_assert(jc::Derived<int>{}.get_value() == 1);
  static_assert(jc::Derived<bool>{}.get_value() == 2);
}
  • 使用作用域运算符不会访问虚函数
#include <cassert>

namespace jc {

template <typename>
struct Base {
  virtual int f() const { return 1; }
};

template <typename T>
struct Derived : Base<T> {  // dependent base
  virtual int f() const { return 2; }
  int get_value() const { return Base<T>::f(); }
};

template <>
struct Base<bool> {
  virtual int f() const { return 3; }
};

}  // namespace jc

int main() {
  assert(jc::Derived<int>{}.get_value() == 1);
  assert(jc::Derived<bool>{}.get_value() == 3);
}
  • 如果需要使用虚函数,则只能使用 this-> 或 using 声明
#include <cassert>

namespace jc {

template <typename>
struct Base {
  virtual int f() const { return 1; }
};

template <typename T>
struct Derived1 : Base<T> {  // dependent base
  virtual int f() const { return 2; }
  int get_value() const { return this->f(); }
};

template <typename T>
struct Derived2 : Base<T> {  // dependent base
  using Base<T>::f;
  virtual int f() const { return 2; }
  int get_value() const { return f(); }
};

template <>
struct Base<bool> {
  virtual int f() const { return 3; }
};

}  // namespace jc

int main() {
  assert(jc::Derived1<int>{}.get_value() == 2);
  assert(jc::Derived1<bool>{}.get_value() == 2);
  assert(jc::Derived2<int>{}.get_value() == 2);
  assert(jc::Derived2<bool>{}.get_value() == 2);
}