当前位置: 首页 > 知识库问答 >
问题:

为什么我必须把“template”和“typename”关键字放在哪里?

马德厚
2023-03-14

在模板中,为什么要在从属名称上放置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::inuniondummy行。我相当肯定inUnion是一个从属名称,VC++完全正确地扼杀了它。
我还知道我应该可以在某个地方添加template来告诉编译器inUnion>是一个模板ID。但具体在哪里?然后它是否应该假设inUnion是一个类模板,即inUnion命名的是一个类型而不是一个函数?


共有2个答案

江俊能
2023-03-14

虽然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();
};

可以看出,我们需要disambiguation关键字,即使编译器自己完全弄清楚a::result_type只能是int(因此是一个类型),并且this->g只能是稍后声明的成员模板g(即使a在某个地方显式专门化,但这不会影响该模板中的代码,因此它的含义不会受到稍后对a的专门化影响!)。

为了改善这种情况,在C++11中,当一个类型引用了封闭模板时,语言会跟踪它。要知道,类型必须是通过使用某种形式的名称形成的,该名称是它自己的名称(在上面,aa ::a )。已知由这样的名称引用的类型是当前实例化。如果形成名称的类型是成员/嵌套类(那么a::NestedClassa都是当前实例化),则可能有多个类型都是当前实例化。

基于这个概念,如果CurrentInstantiation::foofooCurrentInstantiationTyped->foo(如a*a=this;a->foo)是当前实例化或其非依赖基类之一的类的成员(只需立即进行名称查找),那么它们都是当前实例化的成员。

如果限定符是当前实例化的成员,则现在不再需要关键字typenametemplate。这里要记住的一个要点是A 仍然是一个依赖于类型的名称(毕竟t也是依赖于类型的)。但是a ::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::F时再次查找D::result_type(即使它在定义时已经找到了它的含义)。如果现在查找结果不同或导致歧义,则程序格式不正确,必须给出诊断。想象一下,如果我们这样定义C会发生什么

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

在实例化d ::f 时,需要编译器捕获错误。因此,您可以从两种情况中获得最好的结果:“延迟”查找保护您,如果您可能遇到依赖的基类的麻烦,以及“立即”查找,使您从typenametemplate中解脱出来。

D的代码中,名称TypeName D::Questionable_Type不是当前实例化的成员。相反,语言将其标记为未知专门化的成员。特别是,当您正在执行dependentTypeName::foodependentTypedName->foo并且依赖类型不是当前实例化(在这种情况下,编译器可以放弃并说“我们稍后将查看foo是什么”,或者它是当前实例化并且在其中找不到名称,或者它的非依赖基类和依赖基类时,总是会出现这种情况。

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

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

在C++03中,允许捕获此错误的语言是因为永远不可能有一种有效的方法来实例化a ::h (无论您给T)。在C++11中,该语言现在有了进一步的检查,以给编译器实现这一规则更多的理由。由于a没有从属基类,而且a没有声明成员Questionable_Type,因此名称a ::Questionable_Type 既不是当前实例化的成员,也不是未知专用化的成员。在这种情况下,代码不可能在实例化时有效地编译,因此语言禁止限定符是当前实例化的名称既不是未知专门化的成员,也不是当前实例化的成员(但是,仍然不需要诊断这种违反)。

您可以在这个答案上尝试一下这些知识,看看在一个真实世界的例子中,上面的定义是否对您有意义(在这个答案中,它们重复得稍微不那么详细)。

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::g时,将应用主导规则,而查找将找到a::f

索嘉石
2023-03-14

(我的C++11答案也见这里)

为了解析一个C++程序,编译器需要知道某些名称是否是类型。下面的示例演示:

t * f;

该如何解析?对于许多语言,编译器不需要知道名称的含义来进行解析,而且基本上知道一行代码执行什么操作。然而,在C++中,根据t的含义,上面的解释可能会有很大的不同。如果它是一个类型,那么它将是指针f的声明。然而,如果它不是一个类型,它将是一个乘法。所以C++标准在第(3/7)段中说:

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

如果t引用了模板类型参数,编译器将如何找出名称t::x引用了什么?x可以是一个可以被乘以的静态int数据成员,也可以是一个可以产生声明的嵌套类或typedef。如果一个名称具有这样的属性--在知道实际的模板参数之前不能查找它--那么它被称为依赖名称(它“依赖”于模板参数)。

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

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

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

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

答案是:我们决定编译器应该如何解析这个。如果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在限定名之前--因此,如果限定名引用类型,就会被认为是理所当然的。

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

还记得上面的开头引用吗?标准还要求对模板进行特殊处理吗?下面这个看起来很无辜的例子:

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是一个模板,因此编译器知道(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

对于那些书架上有厚厚的标准书籍并且想知道我到底在说些什么的人,我将谈谈标准中是如何规定这一点的。

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

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

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

大多数规则是直观的,并且是递归生成的:例如,如果N是依赖于值的表达式或t是依赖类型,则构造为t[N]的类型是依赖类型。有关这方面的详细信息,请参阅有关依赖类型的(14.6.2/1)节、有关依赖类型的表达式的(14.6.2.2)节和有关依赖值的表达式的(14.6.2.3)节。

标准对什么是依赖名称有点不清楚。在一个简单的阅读(你知道,最小意外的原则),它所定义的从属名称是下面的函数名称的特例。但是,由于t::x显然也需要在实例化上下文中查找,因此它也需要是一个从属名称(幸运的是,从C++14年中期开始,委员会已经开始研究如何解决这个令人困惑的定义)。

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

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

标识符只是字符/数字的简单序列,而接下来的两个是运算符+运算符类型形式。最后一种形式是template-name 。所有这些都是名称,按照标准中的常规用法,名称还可以包括限定符,用于说明名称应该在哪个名称空间或类中查找。

与值相关的表达式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不一定总是出现在名称的最后部分。它可以出现在用作作用域的类名之前的中间位置,如下面的示例所示

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

在某些情况下,关键字是禁用的,详情如下

>

  • 不允许在从属基类的名称上编写typename。假设给定的名称是类类型名。对于基类列表和构造函数初始值设定项列表中的名称都是如此:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
    

    在using-declarations中,不可能在最后一个::之后使用template,而且C++委员会说不要研究解决方案。

     template <typename T>
     struct derive_from_Has_type : SomeBase<T> {
        using SomeBase<T>::template type; // error
        using typename SomeBase<T>::type; // typename *is* allowed
     };
    

  •  类似资料:
    • 我正试图从图中的窗体向表插入子层,但为什么不能使用where呢?

    • 好的。我正在引导一个简单的应用程序。我使用的是flow.js。我使用的预置是babel-preset-2015、babel-preset-react和Babel-Preset-Stage-0。我必须在我的。Babelrc和WebPack.config中放置相同的预置,以便所有这些预置都能工作。例如,如果我从WebPack.config中移动它们,我会得到一个错误'React is not defi

    • 问题内容: 为什么字典键必须是不可变的?我正在寻找一个简单明了的原因,为什么Python字典中的键具有该限制。 问题答案: 在我的计算机上,有一个包含大量英语单词的文件: 让我们创建一个字典来存储所有这些单词的长度: 并且,为了踢球,我们将改组原始单词列表: 嗯,滚刀。无论如何…现在我们已经有点混乱了,我们变得有点偏执了(可能出于与渴望滚刀相同的原因),并且我们想检查字典中的所有单词是否都正确。我

    • 问题内容: 当我使用关键字访问类中的非静态变量时,Java不会给出任何错误。但是当我不使用它时,Java会给出一个错误。为什么要使用? 我知道什么时候应该正常使用,但是这个示例与正常用法大不相同。 例: 问题答案: 首先声明变量,然后赋值。该类与此相同: 您无法执行此操作的原因是,在创建对象时尚未定义,但是对象本身(即)及其所有成员变量都存在。 这是每个的说明:

    • 我给出作为输入文件,一些方法将使用它,但是现在我的reader方法无法检测该文件。 我要将文件放置在哪个目录中....我是否必须在中制定任何maven规范,以检测文件是否存在,并且他们必须读取该文件。

    • 问题内容: 我创建了一个用于显示工具提示的指令: 对应功能: 应用于此: 这是我观点的一部分,由拥有者的控制器处理 为什么必须调用才能将更改应用到,该更改是早先声明和初始化的? 问题答案: 因为附加到事件的回调超出了angular的范围;angular不知道该函数何时运行/结束,因此摘要循环永远不会运行。 调用或告诉angular更新绑定并触发任何手表。