首页 文章

我必须在何处以及为何要使用“模板”和“typename”关键字?

提问于
浏览
952

在模板中,我必须在哪里以及为什么要在依赖名称上放置 typenametemplate ?究竟什么是依赖名称?我有以下代码:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

我遇到的问题是 typedef Tail::inUnion<U> dummy 行 . 我非常确定 inUnion 是一个依赖名称,VC在窒息时非常正确 . 我也知道我应该能够在某处添加 template 来告诉编译器inUnion是一个模板ID . 但到底在哪里?那么它应该假设inUnion是一个类模板,即 inUnion<U> 命名一个类型而不是一个函数?

6 回答

  • 125

    为了解析C程序,编译器需要知道某些名称是否是类型 . 以下示例演示了:

    t * f;
    

    该怎么解析?对于许多语言,编译器不需要知道名称的含义就可以解析并基本知道代码行的作用 . 然而,在C中,上述可以产生截然不同的解释,这取决于 t 的含义 . 如果它是一个类型,那么它将是一个指针 f 的声明 . 但是,如果它不是一个类型,它将是一个乘法 . 所以C标准在第3/7段中说:

    某些名称表示类型或模板 . 通常,只要遇到名称,就必须在继续解析包含它的程序之前确定该名称是否表示这些实体之一 . 确定此过程的过程称为名称查找 .

    如果 t 指的是模板类型参数,编译器将如何找出 t::x 所指的名称? x 可以是一个静态int数据成员,可以成倍增加,或者同样可以是一个可以产生声明的嵌套类或typedef . 如果名称具有此属性 - 它可以't be looked up until the actual template arguments are known - then it' s称为依赖名称(它在模板参数上为"depends") .

    您可能建议等到用户实例化模板:

    让我们等到用户实例化模板,然后找出t :: x * f;的真正含义 .

    这将是标准作为可能的实施方法,并且实际上是允许的 . 这些编译器基本上将模板的文本复制到内部缓冲区中,并且只有在需要实例化时,它们才会解析模板并可能检测定义中的错误 . 但是,在模板的作者发生错误的情况下,其他实现选择尽早检查模板并在实例化甚至发生之前尽快给定错误,而不是打扰模板的用户(可怜的同事!) .

    所以必须有一种方法告诉编译器某些名称是类型而某些名称不是 .

    “typename”关键字

    答案是:我们决定编译器应该如何解析它 . 如果 t::x 是一个从属名称,那么我们需要在 typename 前面加上它来告诉编译器以某种方式解析它 . 标准在(14.6 / 2)说:

    模板声明或定义中使用的名称取决于模板参数,假定不命名类型,除非适用的名称查找找到类型名称或名称由关键字typename限定 .

    有许多名称 typename 不是必需的,因为编译器可以在模板定义中使用适用的名称查找,找出如何解析构造本身 - 例如 T *f; ,当 T 是类型模板参数时 . 但是要 t::x * f; 是一个声明,它必须写成 typename t::x *f; . 如果省略该关键字并且该名称被视为非类型,但是当实例化发现它表示类型时,编译器会发出通常的错误消息 . 有时,错误因此在定义时给出:

    // t::x is taken as non-type, but as an expression the following misses an
    // operator between the two names or a semicolon separating them.
    t::x f;
    

    语法仅在限定名称之前允许 typename - 因此,如果他们这样做,那么非限定名称总是被称为引用类型 .

    对于表示模板的名称存在类似的问题,正如介绍性文本所暗示的那样 .

    “template”关键字

    还记得上面的初始引用以及标准如何要求对模板进行特殊处理吗?让我们采取以下无辜的例子:

    boost::function< int() > f;
    

    人类读者可能看起来很明显 . 编译器不是这样 . 想象一下 boost::functionf 的以下任意定义:

    namespace boost { int function = 0; }
    int main() { 
      int f = 0;
      boost::function< int() > f; 
    }
    

    这实际上是一个有效的表达!它使用小于运算符将 boost::function 与零( int() )进行比较,然后使用大于运算符将得到的 boolf 进行比较 . 但是你可能知道, boost::function in real life是一个模板,所以编译器知道(14.2 / 3):

    在名称查找(3.4)发现名称是模板名称后,如果此名称后跟一个<,则<始终作为模板参数列表的开头,并且从不作为名称后跟较少的名称 - 运营商 .

    现在我们回到了与 typename 相同的问题 . 如果在解析代码时我们还不知道名称是否是模板怎么办?我们需要在模板名称前面插入 template ,由 14.2/4 指定 . 这看起来像:

    t::template f<int>(); // call a function template
    

    模板名称不仅可以在 :: 之后发生,而且可以在类成员访问中的 ->. 之后发生 . 您还需要在那里插入关键字:

    this->template f<int>(); // call a function template
    

    依赖关系

    对于那些书架上有厚厚的Standardese书籍且想知道我究竟在说什么的人,我会谈谈在标准中如何指定这一点 .

    在模板声明中,一些构造具有不同的含义,具体取决于用于实例化模板的模板参数:表达式可能具有不同的类型或值,变量可能具有不同的类型,或者函数调用可能最终调用不同的函数 . 通常认为这种构建体取决于模板参数 .

    标准通过构造是否依赖来精确定义规则 . 它将它们分成不同的逻辑组:一个捕获类型,另一个捕获表达式 . 表达式可能取决于它们的值和/或类型 . 所以我们有附加的典型例子:

    • 从属类型(例如:类型模板参数 T

    • 依赖于值的表达式(例如:非类型模板参数 N

    • 依赖于类型的表达式(例如:转换为类型模板参数 (T)0

    大多数规则都是直观的并且是递归构建的:例如,如果 N 是依赖于值的表达式或 T 是依赖类型,则构造为 T[N] 的类型是依赖类型 . 对于依赖类型,可以在 (14.6.2/1 )中读取此细节,对于依赖于类型的表达式,可以在 (14.6.2.2) 中读取此细节,对于依赖于值的表达式,可以在 (14.6.2.3) 中读取 .

    从属名称

    关于究竟什么是依赖名称,标准有点不清楚 . 在一个简单的阅读(你知道,最小惊喜的原则),它定义为一个从属名称是下面的函数名称的特殊情况 . 但是,由于显然 T::x 也需要在实例化环境中查找,它也需要是一个依赖名称(幸运的是,截至C14中期,委员会已经开始研究如何解决这个令人困惑的定义) .

    为了避免这个问题,我采用了对标准文本的简单解释 . 在表示依赖类型或表达式的所有构造中,它们的子集代表名称 . 因此,这些名称是“依赖名称” . 名称可以采用不同的形式 - 标准说:

    名称是使用标识符(2.11),operator-function-id(13.5),conversion-function-id(12.3.2)或template-id(14.2)表示实体或标签(6.6.4) ,6.1)

    标识符只是一个简单的字符/数字序列,而接下来的两个是 operator +operator type 表单 . 最后一个表格是 template-name <argument list> . 所有这些都是名称,并且通过标准中的常规用法,名称还可以包括限定符,这些限定符表示应该查找名称的名称空间或类 .

    值依赖表达式 1 + N 不是名称,但 N 是 . 作为名称的所有依赖构造的子集称为依赖名称 . 但是,函数名称在模板的不同实例化中可能具有不同的含义,但不幸的是,这个一般规则并未捕获 .

    从属函数名称

    主要不是本文的关注点,但仍值得一提:函数名是一个单独处理的例外 . 标识符函数名称不依赖于它本身,而是依赖于调用中使用的类型相关参数表达式 . 在示例 f((T)0) 中, f 是从属名称 . 在标准中,这在 (14.6.2/1) 中指定 .

    补充说明和示例

    在足够的情况下,我们需要 typenametemplate . 您的代码应如下所示

    template <typename T, typename Tail>
    struct UnionNode : public Tail {
        // ...
        template<typename U> struct inUnion {
            typedef typename Tail::template inUnion<U> dummy;
        };
        // ...
    };
    

    关键字 template 不是't always have to appear in the last part of a name. It can appear in the middle before a class name that'用作范围,如下例所示

    typename t::template iterator<int>::value_type v;
    

    在某些情况下,禁止使用关键字,详情如下

    • 在依赖基类的名称上,不允许编写 typename . 假设给定的名称是类类型名称 . 对于基类列表和构造函数初始化列表中的两个名称都是如此:
    template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
    
    • 在使用声明中,在最后 :: 之后不能使用 template ,而C委员会said不能使用解决方案 .
    template <typename T>
     struct derive_from_Has_type : SomeBase<T> {
        using SomeBase<T>::template type; // error
        using typename SomeBase<T>::type; // typename *is* allowed
     };
    
  • 78

    C 11

    问题

    虽然C 03中关于何时需要 typenametemplate 的规则在很大程度上是合理的,但其制定有一个令人讨厌的缺点

    template<typename T>
    struct A {
      typedef int result_type;
    
      void f() {
        // error, "this" is dependent, "template" keyword needed
        this->g<float>();
    
        // OK
        g<float>();
    
        // error, "A<T>" is dependent, "typename" keyword needed
        A<T>::result_type n1;
    
        // OK
        result_type n2; 
      }
    
      template<typename U>
      void g();
    };
    

    可以看出,即使编译器能够完美地发现自己 A::result_type 只能是 int (因此是一个类型),我们也需要消歧关键字,而且 this->g 只能是成员模板 g 稍后声明(即使 A 在某处显式专门化,也不会影响该模板中的代码,因此其含义不会受到 A 的后期专业化的影响!) .

    当前实例化

    为了改善这种情况,在C 11中,语言跟踪何时类型引用封闭模板 . 要知道,类型必须是通过使用某种形式的名称形成的,这是其自己的名称(在上面, AA<T>::A<T> ) . 已知这种名称引用的类型是当前实例化 . 如果形成名称的类型是成员/嵌套类(那么, A::NestedClassA 都是当前实例化),则可能有多种类型都是当前实例化 .

    根据这一概念,该语言表示 CurrentInstantiation::FooFooCurrentInstantiationTyped->Foo (例如 A *a = this; a->Foo )都是当前实例化的成员 if ,它们被发现是一个类的成员,该类是当前实例化或其非依赖基类之一(通过立即进行名称查找) .

    如果限定符是当前实例化的成员,则现在不再需要关键字 typenametemplate . 这里要记住的一个关键点是 A<T> 仍然是一个类型相关的名称(毕竟 T 也是类型相关的) . 但 A<T>::result_type 已知是一种类型 - 编译器将"magically"调查这种依赖类型来解决这个问题 .

    struct B {
      typedef int result_type;
    };
    
    template<typename T>
    struct C { }; // could be specialized!
    
    template<typename T>
    struct D : B, C<T> {
      void f() {
        // OK, member of current instantiation!
        // A::result_type is not dependent: int
        D::result_type r1;
    
        // error, not a member of the current instantiation
        D::questionable_type r2;
    
        // OK for now - relying on C<T> to provide it
        // But not a member of the current instantiation
        typename D::questionable_type r3;        
      }
    };
    

    这令人印象深刻,但我们可以做得更好吗?该语言甚至更进一步,并且要求实例在实例化 D::f 时再次查找 D::result_type (即使它已在定义时发现其含义) . 当现在查找结果不同或导致模糊时,程序就会形成错误并且必须给出诊断 . 想象一下,如果我们像这样定义 C 会发生什么

    template<>
    struct C<int> {
      typedef bool result_type;
      typedef int questionable_type;
    };
    

    在实例化 D<int>::f 时,需要编译器来捕获错误 . 所以你得到了两个世界中最好的:"Delayed"查找保护你,如果你可能遇到依赖基类的麻烦,还有"Immediate"查找,让你从 typenametemplate 释放 .

    未知专业化

    D 的代码中,名称 typename D::questionable_type 不是当前实例化的成员 . 相反,该语言将其标记为未知专业化的成员 . 特别是,当您执行 DependentTypeName::FooDependentTypedName->Foo 并且依赖类型不是当前实例时(在这种情况下编译器可以放弃并说“我们稍后会查看 Foo 是什么)或者它是当前实例化并且在它或其非依赖基类中找不到名称,并且还存在依赖基类 .

    想象一下,如果我们在上面定义的 A 类模板中有一个成员函数 h 会发生什么

    void h() {
      typename A<T>::questionable_type x;
    }
    

    在C 03中,语言允许捕获此错误,因为永远不会有一种有效的方法来实例化 A<T>::h (无论你给 T 的任何参数) . 在C 11中,语言现在进一步检查,以便为编译器提供更多理由来实现此规则 . 由于 A 没有依赖基类,并且 A 声明没有成员 questionable_type ,因此名称 A<T>::questionable_type 既不是当前实例化的成员,也不是未知专业化的成员 . 在这种情况下,该代码不应该在实例化时有效编译,因此该语言禁止一个名称,其中限定符是当前实例化既不是未知专业化的成员也不是当前实例化的成员(但是,这种违规行为仍然不需要被诊断出来) .

    例子和琐事

    您可以在this answer上尝试这些知识,看看上面的定义是否对您在一个真实世界的例子中有意义(在该答案中,它们的重复性略有不同) .

    C 11规则使以下有效的C 03代码格式不正确(C委员会不打算这样做,但可能不会修复)

    struct B { void f(); };
    struct A : virtual B { void f(); };
    
    template<typename T>
    struct C : virtual B, T {
      void g() { this->f(); }
    };
    
    int main() { 
      C<A> c; c.g(); 
    }
    

    这个有效的C 03代码会在实例化时将 this->f 绑定到 A::f ,一切都很好 . 然而,C 11会立即将其绑定到 B::f ,并且在实例化时需要进行双重检查,检查查找是否仍然匹配 . 但是,在实例化 C<A>::g 时,Dominance Rule适用,查找将找到 A::f .

  • 17

    前言这篇文章是litb帖子的一个易于阅读的替代品 . 根本目的是一样的;对“何时?”的解释和“为什么?”必须应用typename和template .


    typename和template的目的是什么?

    typenametemplate 可用于声明模板以外的情况 .

    在C中有某些上下文,编译器必须明确告诉如何处理名称,所有这些上下文都有一个共同点;他们至少依赖一个模板参数 .

    我们提到这样的名称,在解释中可能存在歧义,因为; “依赖名称” .

    这篇文章将解释依赖名称和两个关键字之间的关系 .


    一个SNACKET超过1000字

    尝试解释以下功能模板中发生的事情,无论是对自己,朋友还是你的猫;标记为(A)的声明中发生了什么?

    template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }
    

    它可能不像人们想象的那么容易,更具体地说,评估(A)的结果在很大程度上取决于作为模板参数 T 传递的类型的定义 .

    不同的 T 可以彻底改变所涉及的语义 .

    struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
    struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();
    

    The two different scenarios

    • 如果我们用类型X实例化函数模板,就像在(C)中一样,我们将声明一个名为x的指向int的指针,但是;

    • 如果我们用类型Y实例化模板,如在(D)中,(A)将由一个表达式组成,该表达式计算123的乘积乘以一些已经声明的变量x .


    理由

    C标准关心我们的安全和福祉,至少在这种情况下 .

    为了防止实现可能遭受令人讨厌的意外,标准要求我们通过明确说明我们想要将名称视为类型名称或模板的任何地方来明确依赖名称的歧义 . ID .

    如果没有说明,则依赖名称将被视为变量或函数 .


    如何处理相关的名字?

    如果这是一部好莱坞电影,依赖名字将是通过身体接触传播的疾病,立即影响其主人,使其混淆 . 混乱可能会导致一个形式不良的perso-,erhm ..计划 .

    从属名称是直接或间接依赖于模板参数的 any 名称 .

    template<class T> void g_tmpl () {
       SomeTrait<T>::type                   foo; // (E), ill-formed
       SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
       foo.data<int> ();                         // (G), ill-formed    
    }
    

    我们在上面的代码段中有四个依赖名称:

    • E

    • "type" 取决于 SomeTrait<T> 的实例化,其中包括 T ,和;

    • F

    • "NestedTrait" ,这是一个模板ID,取决于 SomeTrait<T> ,和;

    • "type" 在(F)的末尾取决于NestedTrait,它取决于 SomeTrait<T> ,和;

    • G

    • "data" ,看起来像一个成员函数模板,间接是一个依赖名称,因为foo的类型取决于 SomeTrait<T> 的实例化 .

    如果编译器将依赖名称解释为变量/函数(如前所述,如果我们没有明确说明,则会发生什么),语句(E),(F)或(G)都不是有效的 .

    解决方案

    为了使 g_tmpl 具有有效的定义,我们必须明确告诉编译器我们期望(E)中的类型,(F)中的模板ID和类型,以及(G)中的模板ID .

    template<class T> void g_tmpl () {
       typename SomeTrait<T>::type foo;                            // (G), legal
       typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
       foo.template data<int> ();                                  // (I), legal
    }
    

    每次名称表示类型时,所涉及的名称必须是类型名称或名称空间,考虑到这一点,我们很容易看到我们在完全限定名称的开头应用了 typename .

    然而,在这方面是不同的,因为没有办法得出如下的结论; "oh, this is a template, than this other thing must also be a template" . 这意味着我们直接在我们想要处理的任何名称前面应用 template .


    我是否只能在任何名称的前面贴上关键词?

    “我可以在任何名称前面贴上类型名称和模板吗?我不想担心它们出现的上下文......” - 一些C开发人员

    标准中的规则规定,只要您处理限定名称(K),就可以应用关键字,但如果名称不合格,则应用程序格式错误(L) .

    namespace N {
      template<class T>
      struct X { };
    }
    
    N::         X<int> a; // ...  legal
    typename N::template X<int> b; // (K), legal
    typename template    X<int> c; // (L), ill-formed
    

    注意:在不需要的情况下应用typename或template不被视为良好做法;只是因为你可以做某事,并不意味着你应该做 .

    此外,有些情况下 typenametemplate 不允许 explicitly

    • When specifying the bases of which a class inherits

    在派生类的base-specifier-list中写入的每个名称都已被视为类型名称,显式指定 typename 既是格式错误的,也是冗余的 .

    // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };
    
    • When the template-id is the one being referred to in a derived class's using-directive
    struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };
    
  • 19
    typedef typename Tail::inUnion<U> dummy;
    

    但是,我不确定你对inUnion的实现是否正确 . 如果我理解正确,这个类不应该被实例化,因此“失败”选项卡永远不会失败 . 也许最好用简单的布尔值来指示类型是否在union中 .

    template <typename T, typename TypeList> struct Contains;
    
    template <typename T, typename Head, typename Tail>
    struct Contains<T, UnionNode<Head, Tail> >
    {
        enum { result = Contains<T, Tail>::result };
    };
    
    template <typename T, typename Tail>
    struct Contains<T, UnionNode<T, Tail> >
    {
        enum { result = true };
    };
    
    template <typename T>
    struct Contains<T, void>
    {
        enum { result = false };
    };
    

    PS:看看Boost::Variant

    PS2:看看typelists,特别是在Andrei Alexandrescu的书中:Modern C Design

  • 2

    这个答案应该是一个相当简短和甜蜜的答案(部分) Headers 问题 . 如果您想要一个更详细的答案,解释为什么必须将它们放在那里,请到这里 .


    放置 typename 关键字的一般规则主要是在使用模板参数时并且您想要访问嵌套的 typedef 或using-alias,例如:

    template<typename T>
    struct test {
        using type = T; // no typename required
        using underlying_type = typename T::type // typename required
    };
    

    请注意,这也适用于元函数或采用通用模板参数的事物 . 但是,如果提供的模板参数是显式类型,则您不必指定 typename ,例如:

    template<typename T>
    struct test {
        // typename required
        using type = typename std::conditional<true, const T&, T&&>::type;
        // no typename required
        using integer = std::conditional<true, int, float>::type;
    };
    

    添加 template 限定符的一般规则大多相似,除了它们通常涉及本身模板化的结构/类的模板化成员函数(静态或其他),例如:

    鉴于此结构和功能:

    template<typename T>
    struct test {
        template<typename U>
        void get() const {
            std::cout << "get\n";
        }
    };
    
    template<typename T>
    void func(const test<T>& t) {
        t.get<int>(); // error
    }
    

    试图从函数内部访问 t.get<int>() 将导致错误:

    main.cpp:13:11: error: expected primary-expression before 'int'
         t.get<int>();
               ^
    main.cpp:13:11: error: expected ';' before 'int'
    

    因此,在此上下文中,您需要事先使用 template 关键字并将其调用为:

    t.template get<int>()

    这样编译器就会正确地解析它,而不是 t.get < int .

  • 974

    我将来自cplusplus.com的JLBorges的优秀response放在类似的问题中,因为这是我在这个主题上读到的最简洁的解释 .

    在我们编写的模板中,可以使用两种名称 - 依赖名称和非依赖名称 . 从属名称是依赖于模板参数的名称;无论模板参数是什么,非依赖名称都具有相同的含义 . 例如:template <typename T> void foo(T&x,std :: string str,int count)
    {
    //在第二阶段查找这些名称
    //当foo被实例化并且类型T已知时
    x.size(); //依赖名称(非类型)
    T :: instance_count; //依赖名称(非类型)
    typename T :: iterator i; //依赖名称(类型)

    //在第一阶段,
    // T :: instance_count被视为非类型(这是默认值)
    // typename关键字指定将T :: iterator视为类型 .

    //在第一阶段查找这些名称
    std :: string :: size_type s; //非依赖名称(类型)
    std :: string :: npos; //非依赖名称(非类型)
    str.empty(); //非依赖名称(非类型)
    数//非依赖名称(非类型)
    }
    对于模板的每个不同实例化,依赖名称所指的内容可能是不同的 . 因此,C模板需要进行“两阶段名称查找” . 最初解析模板时(在任何实例化发生之前),编译器会查找非依赖名称 . 当发生模板的特定实例化时,模板参数随后就已知,并且编译器查找依赖名称 . 在第一阶段,解析器需要知道从属名称是类型的名称还是非类型的名称 . 默认情况下,假定从属名称是非类型的名称 . 从属名称前面的typename关键字指定它是类型的名称 .


    Summary

    仅在模板声明和定义中使用关键字typename,前提是您具有引用类型且取决于模板参数的限定名称 .

相关问题