美文网首页C++ Templates
【C++ Templates(12)】模板中的名称

【C++ Templates(12)】模板中的名称

作者: downdemo | 来源:发表于2018-05-14 09:50 被阅读37次

名称查找

ADL(Argument-Dependent Lookup)

  • 受限名称的名称查找在受限的作用域内进行
int x;
class B {
public:
    int i;
};
class D : public B {
};
void f(D* pd)
{
    pd->i = 3; // finds B::i
    D::x = 2; // ERROR: does not find ::x in the enclosing scope
}
  • 非受限名称先查找该类和基类的作用域再查找外围类的作用域,这种方式也叫普通查找
extern int count; // (1)
int lookup_example(int count) // (2)
{
    if (count < 0) {
    int count = 1; // (3)
    lookup_example(count); // refers to (3)
}
    return count + ::count; // 分别引用(2)、(1)
}
  • 对于非受限名称除了普通查找,还可以使用依赖于参数的查找(ADL,也称Koenig查找)
template <typename T>
inline T const& max (T const& a, T const& b)
{
    return a < b ? b : a;
}

namespace BigMath {
    class BigNumber {
        ...
    };
    bool operator < (BigNumber const&, BigNumber const&);
    ...
}
using BigMath::BigNumber;
void g (BigNumber const& a, BigNumber const& b)
{
    ...
    BigNumber x = ::max(a,b);
    ...
}
  • max()模板不知道BigMath命名空间,普通查找找不到BigNumber类型的operator<,所以需要特殊规则的ADL
  • ADL只能用于非受限名称。对于成员函数名称或类型名称,如果普通查找能找到或把被调用函数名称用圆括号括起来则不会应用ADL
  • 否则,如果名称后的括号里有实参,ADL会查找这些实参的associated namespace和associated class,给定类型的associated namespace和associated class组成的集合定义如下
    • 内置类型:集合为空
    • 指针和数组类型:所引用类型的associated namespace和associated class
    • 枚举类型:associated namespace为枚举声明所在的namespace
    • 类成员:associated class为成员所在的类
    • 类类型:associated class包括该类本身、外围类型、直接和间接基类,assiociated namespace为每个associated class所在的namespace,如果类是一个类模板实例则还包含模板实参本身类型,模板的模板实参所在的类和namespace
    • 函数类型:所有参数类型和返回类型的associated namespace和associated class
    • 某个名为X类的成员指针类型:成员相关的associated namespace和associated class,以及X相关的associated namespace和associated class
  • ADL会忽略using声明
#include <iostream>

namespace X {
    template<typename T> void f(T);
}

namespace N {
    using namespace X;
    enumE { e1 };
    void f(E) {
        std::cout << "N::f(N::E) called\n";
    }
}

void f(int)
{
    std::cout << "::f(int) called\n";
}

int main()
{
    ::f(N::e1); // qualified function name: no ADL
    f(N::e1); // ADL finds N::f(),
}
  • 执行ADL时,命名空间N中的using声明被忽略了,所以一定不会调用X::f

友元名称插入

  • 类中的友元函数声明可以是该函数的首次声明,假设这个类最近的命名空间作用域为A,则可以认为该函数在A中声明,考虑在其他作用域中该友元声明的可见性
template<typename T>
class C {
    ...
    friend void f();
    friend void f(C<T> const&);
    ...
};

void g (C<int>* p)
{
    f(); // Is f() visible here?
    f(*p); // Is f(C<int> const&) visible here?
}
  • 问题在于如果友元声明在外围类可见,则实例化一个类模板可能会使一些普通函数的声明(如f())可见,因此标准规定友元声明在外围作用域不可见
  • 但如果友元函数所在类属于ADL的关联类集合,则在外围类可以找到该友元声明
  • 调用f()没有associated class或associated namespace,因为没有参数不能利用ADL,所以是一个无效调用
  • f(*p)具有关联类C<int>,因此只要调用前实例化了类C<int>就能找到该友元,为了确保这点,如果涉及关联类中友元查找的调用,没被实例化的类会被实例化

插入式类名称

  • 在类作用域中插入类本身的名称,则该名称称为插入式类名称,可被看作位于该类作用域的非受限名称
#include <iostream>

int C;

class C {
private:
    int i[2];
public:
    static int f() {
        return sizeof(C);
    }
};

int f()
{
    return sizeof(C);
}

int main()
{
    std::cout << "C::f() = " << C::f() << "," // 类C的大小
        << " ::f() = " <<::f() << '\n'; // int的大小
}
  • 类模板也可以有插入式类名称,如果后面没有紧跟模板实参列表则视为用参数作实参(如下面的C被看作C<T>),但不会被看作模板名称(如X<C>),为了避免这种情况则需要加上作用域限定符(如X< ::C>)
template<template<typename> class TT> class X {
};

template<typename T> class C {
    C a; // OK: same as ''C<T> a;''
    C<void> b; // OK
    X<C> c; // 错误:C后没有实参列表,不被看作模板
    X<::C> d; // 错误: <:是[的另一种标记
    X< ::C> e; // OK: the space between < and :: is required
};
  • 新标准中已经进行了完善
template<template<typename> class TT> class X {
};

template<typename T> class C {
    C* a;       // OK: same as “C<T>* a;”
    C<void>& b; // OK
    X<C> c;     // OK: C without a template argument list denotes the template C
    X<::C> d;   // OK: ::C is not the injected class name and therefore always
               //     denotes the template
};
  • 如果插入式类名称直接由可变参数模板的模板参数作为实参,则插入式类名称将包含未扩展的模板参数包
template<int I, typename... T> class V {
    V* a;         // OK: same as “V<I, T...>* a;”
    V<0, void> b; // OK
};
  • 实际上一个类或类模板的插入式类名称是被定义的类型的一个别名。对于非模板类,这是显而易见的,因为类本身是唯一的具有该名称且在该范围内的类型。但是,在类模板或类模板内的嵌套类中,每个模板实例化都会产生一个不同的类型。这意味着插入式类名称代表类模板的相同实例化,而不是一些其他的类模板特化(对类模板的嵌套类同样适用)
  • 在一个类模板中,插入式类名称或任何等价于enclosing类或类模板的插入式类名称的类型,被称为current instantiation,依赖于一个模板参数但是不是current instantiation的被称为一个unknown specialization,它可能由相同的类模板或一些完全不同的类模板实例化。下面说明了这两个概念的区别
template<typename T> class Node {
    using Type = T;
    Node* next;           // Node refers to a current instantiation
    Node<Type>* previous; // Node<Type> refers to a current instantiation
    Node<T*>* parent;     // Node<T*> refers to an unknown specialization
};
  • 在嵌套类和类模板存在的情况下,识别一个类型是否为current instantiation容易产生混淆。enclosing class和类模板的注入式类名称是一个current instantiation,而其他嵌套类或类模板的注入式类名称则不是
template<typename T> class C {
    using Type = T;

    struct I {
        C* c;               // C refers to a current instantiation
        C<Type>* c2;        // C<Type> refers to a current instantiation
        I* i;               // I refers to a current instantiation
    };
    struct J {
        C* c;               // C refers to a current instantiation
        C<Type>* c2;        // C<Type> *refers to a current instantiation
        I* i;               // I refers to an unknown specialization,
                        // because I does not enclose
        JJ* j;              // J refers to a current instantiation
    };
};
  • 当一个类型是一个current instantiation,实例化的类的内容将保证由当前定义的类模板或嵌套类实例化而来,这将对解析模板时的名称查找产生影响,但也会导致一种更灵活的方式来决定一个类模板定义中的类型X是一个current instantiation还是unknown specialization:如果另一个程序员能写一个显式特化,则X就是那个特化,否则就是一个unknown specialization
  • 举例来说,考虑上例中类型C<int>::J的实例化,C<int>::J的定义用于实例化具体类型,此外,因为一个显式特化不能特化一个模板或模板成员,也不会特化所有的enclosing模板或成员,C<int>将由enclosing class的定义实例化,因此,在J中,J和C<int>代表一个current instantiation,另一方面,C<int>::I 的显式特化可以写为如下
template<> struct C<int>::I {
    // definition of the specialization
};
  • 这里C<int>::I 的特化提供了一个和C<T>::J完全不同的定义,因此I在C<T>::J中的定义是一个unknown specialization

解析模板

非模板中的上下文相关性

  • 解析理论主要面向上下文无关语言,而C++是上下文相关语言,为了解决这个问题,编译器使用一张符号表结合扫描器和解析器
  • 解析某个声明时会把它添加到表中,扫描器找到一个标识符时,会在符号表中查找,如果发现该符号是一个类型就会注释这个标记,如编译器看见
x*
  • 扫描器会查找x,如果发现x是一个类型,解析器会看到
identifier, type, x
symbol, *
  • 并认为这里进行了一个声明,如果x不是类型,则解析器从扫描器获得标记为
identifier, nontype, x
symbol, *
  • 于是这个构造就被解析为乘积
  • 下面的表达式是一个上下文相关的例子
X<1>(0)
  • 如果X是类模板名称,则表达式是把0转换成X<1>类型。如果不是模板的话,表达式等价于
(X<1)>0
  • 先让X和1比大小,然后把结果转换成1或0,再与0比较大小。因此解析器先查找<前的名称,是模板名称时才会把<看作左尖括号,其他情况则看作小于号
template<bool B>
class Invert {
public:
    static bool const result = !B;
};
void g()
{
    bool test = B<(1>0)>::result; // parentheses required!
}
  • 如果省略B<(1>0)>中的圆括号,第一个>会被错误地看作模板实参列表的结束标记,编译器把代码等价地看作((B<1>))0>::result,这会使这段代码无效
  • 引入嵌套template-id时也是同理
List<List<int>> a;
  • 由maximum munch扫描原则,如果两个大于号之间没有空格会被组合成右移标记>>,另一个例子就是上述中使用尖括号时遇到作用域运算符
class X {
...
};
List< ::X> many_X;
  • 在C++11中,上述问题已经被完善
template<typename T> struct G {};
struct S;
G<::S> gs;               // valid since C++11, but an error before that

#define F(X) X ## :

int a[] = { 1, 2, 3 }, i = 1;
int n = a F(<::)i];       // valid in C++98/C++03, but not in C++11

依赖型类型名称

  • 模板名称的问题主要是不能有效确定名称,模板中不能引用其他模板的名称,因为其他模板的内容可能由于显式特化使原来的名称失效
template<typename T>
class Trap {
public:
    enum{x}; // (1) x is not a type here
};

template<typename T>
class Victim {
public:
    int y;
    void poof() {
        Trap<T>::x*y; // (2) declaration or multiplication?
    }
};

template<>
class Trap<void> { // evil specialization!
public:
    typedef int x; // (3) x is a type here
};

void boom(Trap<void>& bomb)
{
    bomb.poof();
}
  • 编译器解析(2)时,要确定看到的是声明还是乘积,取决于依赖型受限名称Trap<T>::x是否为类型名称,编译器此时查找模板Trap,根据(1),Trap<T>::x不是类型,因此(2)被看作乘积。而在T为void的特化中让x变成了类型,完全违背了前面的代码,这种情况下类Victim中的Trap<T>::x实际上是一个int类型
  • 为了解决这个冲突,通常依赖型受限名称不会被看作类型,当类型名称前有以下性质时,必须在名称前加上typename关键字
    • 名称出现在一个模板中
    • 名称是受限的
    • 名称不在用于指定基类继承的列表中,也不位于引入构造函数的成员初始化列表中
    • 名称依赖于模板参数
  • 只有前三个条件同时满足时才能用typename前缀。考虑下面这个错误的例子
template<typename T>
struct S: typename X<T>::Base {
    S(): typename X<T>::Base(typename X<T>::Base(0)) {}
    typename X<T> f() {
        typename X<T>::C * p; // declaration of pointer p
        X<T>::D * q; // multiplication!
    }
    typename X<int>::C * s;
};

struct U {
    typename X<int>::C * pc;
};
  • 第1个typename引入一个模板参数,不适用前面的规则
  • 第2、3个是规则禁止的用法(指定基类继承的列表中)
  • 第4个是必需的,满足条件,目的是基于实参0构造一个X<T>::Base表达式(或类型转换)
  • 第5个是禁止的,因为后面的X<T>不是受限名称
  • 第6个如果是为了声明一个指针,则是必需的
  • 第7个可有可无,因为符合前三个规则但不符合第四个
  • 第8个是禁止的,因为它不是在模板中使用

依赖型模板名称

  • 编译器会把模板名称后的<看作模板参数列表的开始,要在名称前加template关键字,才能让编译器知道引用的依赖型名称是一个模板。下面的例子说明了如何在运算符(::,->和.)后使用template关键字
template<typename T>
class Shell {
public:
    template<int N>
    class In {
        public:
        template<int M>
        class Deep {
            public:
                virtual void f();
        };
    };
};

template<typename T, int N>
class Weird {
public:
    void case1(Shell<T>::template In<N>::template Deep<N>* p) {
        p->template Deep<N>::f(); // 禁止虚函数调用
    }
    void case2(Shell<T>::template In<T>::template Deep<T>& p) {
        p.template Deep<N>::f(); // 禁止虚函数调用
    }
};
  • 如果运算符前的名称或表达式类型要依赖于某个模板参数,紧接在运算符后的是一个template-id,就应该使用template关键字
p.template Deep<N>::f()
  • p类型依赖于模板参数T,如果不加template,编译器不会查找Deep类判断是否为模板,p.Deep<N>::f()会被解析为((p.Deep)<N)>f()

using声明中的依赖型名称

  • using声明会从类和命名空间引入名称。如果引入的是命名空间则不会涉及上下文问题,因为不存在名字空间模板。从类中引入名称的using声明能力有限,只能把基类中的名称引入到派生类中
class BX {
public:
    void f(int);
    void f(char const*);
    void g();
};
class DX : private BX {
public:
    using BX::f;
};
  • using声明从依赖型类中引入名称时,并不知道该名称是类型名称、模板名称还是其他名称
template<typename T>
class BXT {
public:
    using Mystery = T;
    template<typename U>
    struct Magic;
};

template<typename T>
class DXTT : private BXT<T> {
public:
    using typename BXT<T>::Mystery;
    Mystery* p; // would be a syntax error if not for the typename
};
  • 如果希望using声明引入的依赖型名称是一个类型,必须用typename关键字显式指定,但标准没有提供机制来指定依赖型名称是一个模板(新标准中解决了这个问题)
template<typename T>
class DXTM : private BXT<T> {
public:
    using BXT<T>::template Magic; // ERROR: not standard
    Magic<T>* plink; // ERROR: Magic is not a known template
};
  • C++11的别名模板提供了一个解决方法
template<typename T>
class DXTM : private BXT<T> {
public:
    template<typename U>
    using Magic = typename BXT<T>::template Magic<T>; // Alias template
    Magic<T>* plink;                                    // OK
};

ADL和显式模板实参

namespace N {
class X {
        ...
    };
    template<int I> void select(X*);
}

void g (N::X* xp)
{
    select<3>(xp); // ERROR: no ADL!
}
  • 上述代码调用elect<3>(xp)时,不会通过ADL找到模板select(),因为编译器在不知道<3>是模板实参列表前无法判断xp是函数调用实参,要推断<3>是模板实参列表则要先知道select()是模板,这就陷入了死循环,编译器最终把表达式解析成(select<3)>(xp),这是无意义的

派生和类模板

非依赖型基类

  • 类模板中,非依赖型基类指无需知道模板实参就可以推断类型的基类
template<typename X>
class Base {
public:
    int basefield;
    using T = int;
};

class D1: public Base<Base<void> > { // 实际上不是模板
public:
    void f() { basefield = 3; }
};

template<typename T>
class D2 : public Base<double> { // 非依赖型基类
public:
    void f() { basefield = 7; }
    T strange; // T是Base<double>::T,而非模板参数
};
  • 对于模板的非依赖型基类,如果在派生类中查找一个非受限名称则会先查找这个非依赖型基类,然后才会查找模板参数列表。上例中,类模板D2的成员strange类型总会是Base<double>::T中对应的T类型(即int),比如下面的代码就是无效的
void g (D2<int*>& d2, int* p)
{
    d2.strange = p; // 错误:类型不匹配
}

依赖型基类

  • 对于模板中的非依赖型名称,编译器会在看到的第一时间查找
template<typename T>
class DD : public Base<T> { // dependent base
public:
    void f() { basefield = 0; } // (1) problem...
};

template<> // explicit specialization
class Base<bool> {
public:
    enum { basefield = 42 }; // (2) tricky!
};

void g (DD<bool>& d)
{
    d.f(); // (3) oops?
}
  • (1)发现非依赖型名称basefield,在模板Base中找到,把basefield绑定到int变量,然而随后的显式特化改写了Base的泛型定义,改变了basefield的含义(但此时basefield已经确定绑定为一个int变量),在(3)实例化DD::f定义时就会产生错误
  • 为了解决这个问题,标准规定非依赖型名称不会在依赖型基类中查找,因此编译器会在(1)给出诊断信息,为了纠正可以让basefield也成为依赖型名称(实例化时才会查找)
// 方案1:
template<typename T>
class DD1 : public Base<T> {
public:
    void f() { this->basefield = 0; } // lookup delayed
};

// 方案2:利用受限名称引入依赖性
template<typename T>
class DD2 : public Base<T> {
public:
    void f() { Base<T>::basefield = 0; }
};
  • 使用方案2要注意,如果原来的非受限的非依赖型名称用于虚函数调用,这种引入依赖性的限定会禁止虚函数调用,从而改变程序含义。对于方案2不适用的情况,使用方案1
template<typename T>
class B {
public:
    enumE { e1=6, e2=28, e3=496 };
    virtual void zero(E e = e1);
    virtual void one(E&);
};

template<typename T>
class D : public B<T> {
public:
    void f() {
        typename D<T>::E e; // this->E会是一个无效的语法
        this->zero(); // D<T>::zero()会禁止虚函数调用
        one(e); // one是依赖型名称,因为实参e类型(D<T>::E)是依赖型
    }
};
  • 如果觉得重复的限定显得代码混乱,可以在派生类中只引入依赖型基类中的名称一次
// 方案3:
template<typename T>
class DD3 : public Base<T> {
public:
    using Base<T>::basefield; // dependent name now in scope
    void f() { basefield = 0; } // fine
};

相关文章

网友评论

    本文标题:【C++ Templates(12)】模板中的名称

    本文链接:https://www.haomeiwen.com/subject/nrkddftx.html