代码之家  ›  专栏  ›  技术社区  ›  MSalters

我为什么要把“模板”和“类型名”关键字放在哪里?

  •  965
  • MSalters  · 技术社区  · 16 年前

    在模板中,我必须放置在哪里和为什么 typename template 依赖的名字?究竟什么是从属名称?我有以下代码:

    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++完全正确地扼杀了它。我也知道我应该能够 模板 告诉编译器inunion是一个模板ID的地方。但是具体在哪里呢?然后假设inunion是一个类模板,即 inUnion<U> 命名类型而不是函数?

    6 回复  |  直到 6 年前
        1
  •  995
  •   Alan jiggunjer    8 年前

    为了分析C++程序,编译器需要知道某些名称是否为类型。以下示例说明:

    t * f;
    

    这应该如何解析?对于许多语言来说,编译器不需要知道名称的含义,就可以解析和基本了解一行代码的作用。在C++中,以上所述可以根据不同的原因产生非常不同的解释。 t 手段。如果它是一个类型,那么它将是指针的声明 f . 然而,如果它不是一个类型,它将是一个乘法。因此,C++标准在第(3/7)段中表示:

    有些名称表示类型或模板。通常,每当遇到一个名称时,在继续分析包含该名称的程序之前,必须确定该名称是否表示这些实体之一。确定这一点的过程称为名称查找。

    编译器将如何确定名称 t::x 指,如果 T 是否引用模板类型参数? x 可以是静态int数据成员,该成员可以被相乘,也可以是嵌套类或typedef,该类或typedef可以生成声明。如果一个名称具有此属性(在知道实际模板参数之前无法查找它),则称为 从属名 (取决于模板参数)。

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

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

    这将起作用,并且作为一种可能的实现方法被标准实际允许。这些编译器基本上将模板的文本复制到一个内部缓冲区中,并且只有在需要实例化时,才会解析模板并可能检测到定义中的错误。但是不要打扰模板的用户(可怜的同事!)对于模板作者所犯的错误,其他实现选择尽早检查模板,并在实例化发生之前尽快给出定义中的错误。

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

    “typename”关键字

    答案是: 我们 决定编译器应该如何分析这个问题。如果 T::X 是一个从属名称,然后我们需要在其前面加前缀 typename 告诉编译器以某种方式解析它。标准规定为(14.6/2):

    模板声明或定义中使用的依赖于模板参数的名称是 假定不命名类型,除非适用的名称查找找到类型名称或名称是限定的 通过关键字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;
    

    语法允许 类别名 仅限限定名之前 -因此,不合格名称通常被认为是指类型(如果它们是这样做的话)。

    类似的gotcha存在于表示模板的名称中,正如介绍性文本所暗示的那样。

    “template”关键字

    记住上面的初始报价,以及标准如何要求对模板进行特殊处理?让我们以下面这个无辜的例子为例:

    boost::function< int() > f;
    

    对于人类读者来说,这可能是显而易见的。对编译器来说不是这样。想象一下以下任意定义 boost::function f :

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

    这实际上是一个有效的 表达 !它使用小于运算符进行比较 Boo::函数 反零(零) int() ,然后使用大于运算符比较结果 bool 反对 f . 不过,正如你所知, Boo::函数 in real life 是一个模板,因此编译器知道(14.2/3):

    在名称查找(3.4)发现名称是模板名称后,如果此名称后跟一个<,则<。 始终作为模板参数列表的开头,从不作为名称后跟小于 操作员。

    现在我们又回到了同样的问题上 类别名 . 如果在解析代码时我们还不知道名称是否是模板呢?我们需要插入 template 紧接模板名称之前,由指定 14.2/4 . 这看起来像:

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

    模板名称不能只出现在 :: 但也在 -> . 在类成员访问中。您还需要在此处插入关键字:

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

    依赖关系

    对于那些书架上有厚厚的标准书籍的人,如果他们想知道我到底在说什么,我会稍微谈一谈标准中如何规定这一点。

    在模板声明中,根据用于实例化模板的模板参数,某些构造具有不同的含义:表达式可能具有不同的类型或值,变量可能具有不同的类型,或者函数调用可能最终调用不同的函数。这种构造通常被称为 依赖 在模板参数上。

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

    • 依赖类型(例如:类型模板参数 T )
    • 依赖于值的表达式(例如:非类型模板参数 N )
    • 依赖于类型的表达式(例如:类型模板参数的强制转换 (T)0 )

    大多数规则都是直观的,并且是递归构建的:例如,类型构造为 T[N] 是依赖类型,如果 n 是依赖于值的表达式或 T 是依赖类型。有关详细信息,请参阅 (14.6.2/1 )对于从属类型, (14.6.2.2) 对于依赖类型的表达式和 (14.6.2.3) 对于值相关表达式。

    从属名

    标准对什么有点不清楚 确切地 是一个 从属名 . 在一个简单的阅读(你知道,最不惊讶的原则)中,它定义为 从属名 以下是函数名的特殊情况。但很明显 T::x 还需要在实例化上下文中查找,它还需要是一个依赖的名称(幸运的是,正如C++中的14,委员会已经开始研究如何修复这个令人困惑的定义)。

    为了避免这个问题,我对标准文本进行了简单的解释。在所有表示依赖类型或表达式的构造中,一个子集表示名称。因此,这些名称是“从属名称”。一个名字可以采用不同的形式-标准规定:

    名称是使用标识符(2.11)、运算符函数ID(13.5)、转换函数ID(12.3.2)或模板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) .

    附加说明和示例

    在足够的情况下,我们需要 类别名 模板 . 您的代码应该如下所示

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

    关键词 模板 不必总是出现在名称的最后一部分。它可以出现在用作作用域的类名之前的中间,如下例所示

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

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

    • 不允许在依赖基类的名称上写入 类别名 . 假定给定的名称是类类型名称。对于基类列表和构造函数初始值设定项列表中的两个名称都是这样的:

       template <typename T>
       struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
       { };
      
    • 在使用声明时,不可能使用 模板 最后一次 :: 和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
       };
      
        2
  •  127
  •   Community CDub    8 年前

    C++ 11

    问题

    而C++ 03中的规则在你需要的时候 typename template 在很大程度上是合理的,它的公式有一个令人讨厌的缺点

    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 是在某个地方显式专门化的,不会影响该模板中的代码,因此其含义不会受到以后的 !).

    电流实例化

    为了改善这种情况,在C++ 11中,当一个类型引用封闭模板时,语言会跟踪。要知道,类型必须是通过使用某种形式的名称形成的,这是它自己的名称(在上面, , A<T> , ::A<T> )由此类名称引用的类型已知为 电流实例化 . 如果从中形成名称的类型是成员/嵌套类(那么, A::NestedClass 都是当前的实例化)。

    基于这个概念,语言说 CurrentInstantiation::Foo , Foo CurrentInstantiationTyped->Foo (如 A *a = this; a->Foo 都是 当前实例化的成员 如果 它们被发现是当前实例化的类或其非依赖基类之一的成员(只需立即执行名称查找)。

    关键词 类别名 模板 如果限定符是当前实例化的成员,则现在不再需要。这里要记住的一个关键点是 A & T;T & GT; 仍然 依赖类型的名称(毕竟 T 也依赖于类型)。但是 A<T>::result_type 已知是一个类型-编译器将“神奇地”研究这种依赖类型来解决这个问题。

    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::result_type 实例化时 D::f (即使它在定义时已经找到了它的含义)。当查找结果不一致或产生歧义时,程序格式错误,必须给出诊断。想象一下如果我们定义 C 这样地

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

    在实例化时需要一个编译器来捕获错误 D<int>::f . 因此,您可以从这两个方面得到最好的结果:“延迟的”查找可以保护您,如果您在依赖的基类中遇到问题,也可以使用“立即的”查找将您从中解放出来。 类别名 模板 .

    未知的专业

    在代码中 D ,这个名字 typename D::questionable_type 不是当前实例化的成员。相反,语言将其标记为 未知专业的成员 . 尤其是,当你这样做的时候 DependentTypeName::Foo DependentTypedName->Foo 依赖类型为 当前的实例化(在这种情况下,编译器可以放弃并说“我们稍后将查看 IS) 当前的实例化和名称在它或它的非依赖基类中找不到,并且还有依赖的基类。

    想象一下如果我们有一个成员函数会发生什么 h 在上述定义范围内 类模板

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

    在C++ 03中,允许语言捕获此错误,因为永远不可能有实例化的有效方式。 A<T>::h (不管你怎么说 T )在C++ 11中,语言现在有了进一步的检查,以便为编译器实现这个规则提供更多的理由。自从 没有依赖的基类,并且 声明没有成员 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 , the Dominance Rule 应用并查找 答:F 相反。

        3
  •  81
  •   Community CDub    8 年前

    序言

    这篇文章的目的是 易于阅读 替代 litb's post .

    基本目的是相同的;对“何时”的解释。“为什么?” typename template 必须应用。


    目的是什么 类别名 模板 ?

    类别名 模板 在声明模板以外的情况下可用。

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

    在解释中可能存在歧义的情况下,我们称之为: 从属名 “。

    这篇文章将解释 从属名 和两个关键字。


    一个片段可以说1000多个单词

    试着解释下面发生了什么 功能模板 或者对你自己,朋友,或者你的猫;在标记的陈述中发生了什么( )?

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


    这可能不像人们想象的那么容易,更具体地说,评估的结果( 沉重地 取决于 关于作为模板参数传递的类型的定义 T .

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

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


    两种不同的情况 :

    • 如果我们用类型实例化函数模板 X ,如 C )的声明 指向int的指针 命名 X 但是;

    • 如果我们用类型实例化模板 Y ,如 D ) )而是由计算 一百二十三 与已声明的变量相乘 X .



    理据

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

    为了防止一个实现可能遭受令人讨厌的意外,标准要求我们解决 从属名 通过 明确地 在我们希望将名称视为 类型名 ,或者 模板ID .

    如果没有说明,则 从属名 将被视为变量或函数。



    如何处理 从属名 ?

    如果这是一部好莱坞电影, 从属名 将是通过身体接触传播的疾病,瞬间影响其宿主使其混淆。可能会导致一个不健全的人,二胡的困惑。程序。

    从属名 任何 直接或间接依赖于 模板参数 .

    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 )
      • “类型” 取决于 SomeTrait<T> 其中包括 T ;
    • f )
      • “嵌套特征” ,这是一个 模板ID 取决于 某些特性 ;
      • “类型” 结束时( f 取决于 嵌套性状 ,这取决于 某些特性 ;
    • G )
      • “数据” ,看起来像 成员函数模板 ,是间接的 从属名 因为 取决于 某些特性 .

    两种说法都没有( e ) f (或) G )如果编译器将解释 从属名 作为变量/函数(如前所述,如果我们不明确地说其他的话,就会发生这种情况)。

    解决方案

    使 g_tmpl 有一个有效的定义,我们必须明确地告诉编译器我们需要一个( e a) 模板ID 和A 类型 in( f )和 模板ID in( G )

    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
    }
    

    每一次 名称 表示类型, 全部的 姓名 参与必须是 类型名称 命名空间 考虑到这一点,很容易看出我们的申请 类别名 在我们全面的开始 限定名 .

    模板 然而,在这方面是不同的,因为没有办法得出这样的结论; “哦,这是一个模板,而另一个必须也是模板” . 这意味着我们申请 模板 直接在任何 名称 我们愿意这样对待。



    我能不能把 关键词 在任何名字前面?

    我能坚持吗 类别名 模板 在任何名字前面?我不想担心他们出现的环境… “- Some C++ Developer

    标准中的规则规定,只要您处理的是 限定名 ( 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
    

    注释 应用 类别名 模板 在一个不需要它的环境中,并不认为是一种良好的实践;仅仅因为你可以做一些事情,并不意味着你应该做。


    此外,还有一些上下文 类别名 模板 明确地 不允许的:

    • 指定类继承的基时

      派生类中写入的每个名称 基本说明符列表 已被视为 类型名 ,显式指定 类别名 既不规范又冗余。

                         // .------- the base-specifier-list
       template<class T> // v
       struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
         ...
       };
      


    • 模板ID 在派生类的 使用指令

       struct Base {
         template<class T>
         struct type { };
       };
      
       struct Derived : Base {
         using Base::template type; // ill-formed
         using Base::type;          // legal
       };
      
        4
  •  19
  •   Luc Touraille    16 年前
    typedef typename Tail::inUnion<U> dummy;
    

    但是,我不确定您是否正确实现了inunion。如果我理解正确,这个类不应该被实例化,因此“fail”选项卡永远不会实际失败。也许最好用一个简单的布尔值来指示类型是否在联合中。

    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 };
    };
    

    附言:看看 Boost::Variant

    PS2:看看 typelists 尤其是在Andrei Alexandrescu的书中:现代C++设计

        5
  •  17
  •   Community CDub    8 年前

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


    typename 关键字主要是在使用模板参数并且要访问嵌套的 typedef 或者使用别名,例如:

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

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

    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'
    

    因此,在这种情况下,您需要 模板 预先输入关键字,然后这样称呼它:

    t.template get<int>()

    这样编译器就可以正确地分析这个问题,而不是 t.get < int .

        6
  •  2
  •   KeyC0de    6 年前

    我把杰博尔赫斯的优秀 response 从cplusplus.com逐字回答一个类似的问题,因为这是我读过的关于这个主题的最简洁的解释。

    在我们编写的模板中,有两种名称可以使用——依赖名称和非依赖名称。从属名称是依赖于模板参数的名称;非从属名称的含义与模板参数的含义相同。

    例如:

    template< typename T > void foo( T& x, std::string str, int count )
    {
        // these names are looked up during the second phase
        // when foo is instantiated and the type T is known
        x.size(); // dependant name (non-type)
        T::instance_count ; // dependant name (non-type)
        typename T::iterator i ; // dependant name (type)
    
        // during the first phase, 
        // T::instance_count is treated as a non-type (this is the default)
        // the typename keyword specifies that T::iterator is to be treated as a type.
    
        // these names are looked up during the first phase
        std::string::size_type s ; // non-dependant name (type)
        std::string::npos ; // non-dependant name (non-type)
        str.empty() ; // non-dependant name (non-type)
        count ; // non-dependant name (non-type)
    }
    

    对于模板的每个不同实例化,依赖名称所指的内容可能有所不同。因此,C++模板受到“二相名称查找”的影响。当最初分析模板时(在任何实例化发生之前),编译器将查找非依赖名称。当模板发生特定的实例化时,模板参数就会在那时被知道,编译器会查找相关的名称。

    在第一个阶段中,解析器需要知道从属名称是类型的名称还是非类型的名称。默认情况下,依赖名称假定为非类型的名称。依赖名称之前的type name关键字指定它是类型的名称。


    总结

    仅在模板声明和定义中使用关键字type name,前提是您具有引用类型并依赖于模板参数的限定名称。