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

Where and why do I have to put the "template" and "typename" keywords?

提问人:MSalters 提问时间:3/4/2009 最后编辑:Jan SchultkeMSalters 更新时间:9/28/2023 访问量:258490

问:

在模板中,我必须在哪里以及为什么必须放置和依赖名称?
到底什么是依赖名称?
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; // intentionally not defined
    template< > struct inUnion<T> { };   // specialization only for T
};

我遇到的问题是在线路上。我相当确定这是一个依赖名称,而 VC++ 对它感到窒息是完全正确的。typedef Tail::inUnion<U> dummyinUnion

我也知道我应该能够在某个地方添加以告诉编译器这是一个模板 ID,但究竟在哪里?然后它应该假设这是一个类模板,即 命名一个类型而不是一个函数?templateinUnioninUnioninUnion<U>

C++ 模板 typename c++-faq dependent-name

评论

0赞 Justin L. 8/27/2011
请注意,这使用 typename SomeBase<T>::type;由于 gcc.gnu.org/bugzilla/show_bug.cgi?id=14258,在 GCC 中不起作用。
0赞 Ciro Santilli OurBigBook.com 9/22/2013
这对我的输入有帮助:pages.cs.wisc.edu/~driscoll/typename.html

答:

20赞 Luc Touraille 3/4/2009 #1
typedef typename Tail::inUnion<U> dummy;

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

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:看看类型列表,特别是在Andrei Alexandrescu的书:现代C++设计

评论

0赞 MSalters 3/5/2009
inUnion<U> 将被实例化,例如,如果您尝试使用 U==int 调用 Union<float,bool>::operator=(U)。它调用一个私有集合(U, inUnion<U>* = 0)。
0赞 MSalters 3/5/2009
result=true/false 的工作是我需要 boost::enable_if< >,这与我们当前的 OSX 工具链不兼容。不过,单独的模板仍然是一个好主意。
0赞 Johannes Schaub - litb 3/5/2009
Luc 表示 typedef Tail::inUnion<U> 虚拟;线。这将实例化 Tail。但不是 inUnion<U>。当它需要它的完整定义时,它会被实例化。例如,如果您采用 sizeof 或访问成员(使用 ::foo),就会发生这种情况。@MSalters无论如何,你还有另一个问题:
0赞 Johannes Schaub - litb 3/5/2009
-sizeof(U) 从不为负:)因为size_t是无符号整数类型。你会得到一些非常高的数字。您可能想执行 sizeof(U) >= 1 ?-1 : 1 或类似:)
0赞 Johannes Schaub - litb 3/5/2009
...则 char f[sizeof(U) >= 1 ? -1 : 1] 或 -sizeof(U) 永远无效。我很久以前读过它,但今天早上我又发现了这一段: 14.6/7 .它不需要拒绝它,但它可能会这样做。但是,如果您只放置模板的声明,则没关系。
1418赞 Johannes Schaub - litb 3/5/2009 #2

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

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

t * f;

这应该如何解析?对于许多语言,编译器不需要知道名称的含义即可解析并基本上知道一行代码执行的操作。然而,在 C++ 中,上述内容可能会产生截然不同的解释,具体取决于含义。如果它是一个类型,那么它将是一个指针的声明。但是,如果它不是类型,它将是乘法。所以 C++ 标准在第 (3/7) 段说:tf

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

如果引用模板类型参数,编译器将如何找出名称所指的内容? 可以是可以乘以的静态 int 数据成员,也可以是可以产生声明的嵌套类或 typedef。如果名称具有此属性(在知道实际模板参数之前无法查找),则称为依赖名称(它“取决于”模板参数)。t::xtx

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

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

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

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

“typename”关键字

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

模板声明或定义中使用的依赖于模板参数的名称是 假定不命名类型,除非适用的名称查找找到类型名称或该名称是限定的 通过关键字 typename。

有许多名称不是必需的,因为编译器可以通过模板定义中适用的名称查找来弄清楚如何解析构造本身 - 例如,使用 , when 是类型模板参数。但要成为声明,它必须写成 .如果省略关键字并且名称被视为非类型,但当实例化发现它表示类型时,编译器会发出通常的错误消息。有时,错误会在定义时给出:typenameT *f;Tt::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; 
}

这实际上是一个有效的表达!它使用小于运算符与零 () 进行比较,然后使用大于运算符将结果与 进行比较。但是,您可能很清楚,在现实生活中是一个模板,因此编译器知道 (14.2/3):boost::functionint()boolfboost::function

名称查找 (3.4) 发现名称是 template-name 后,如果此名称后跟 <,则< 始终将其作为模板参数列表的开头,从不作为名称后跟 less-than 算子。

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

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

模板名称不仅可以出现在 a 之后,还可以出现在 或 之后 或 在类成员访问中。您还需要在那里插入关键字:::->.

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

依赖

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

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

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

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

大多数规则都是直观的,并且是递归构建的:例如,构造为 if 的类型是依赖于值的表达式或依赖类型,则为依赖类型。有关依赖类型、类型相关表达式和值相关表达式的 ) 部分,可以阅读这方面的详细信息。T[N]NT(14.6.2/1(14.6.2.2)(14.6.2.3)

从属名称

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

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

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

标识符只是一个普通的字符/数字序列,而接下来的两个是 and 形式。最后一种形式是 。所有这些都是名称,根据标准中的常规用法,名称还可以包含限定符,这些限定符表示应该在哪个命名空间或类中查找名称。operator +operator typetemplate-name <argument list>

与值相关的表达式不是名称,而是名称。作为名称的所有依赖构造的子集称为依赖名称。但是,函数名称在模板的不同实例中可能具有不同的含义,但不幸的是,此一般规则并未捕获。1 + NN

依赖函数名称

本文主要不关注,但仍然值得一提:函数名称是单独处理的例外。标识符函数名称不是由其本身构成的,而是由调用中使用的类型相关参数表达式构成的。在示例中,是一个从属名称。在标准中,这在 中指定。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 
     { };
    
  • 在使用声明中,不可能在最后一个之后使用,并且 C++ 委员会表示不要研究解决方案。template::

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

评论

31赞 Johannes Schaub - litb 11/23/2010
这个答案是从我之前删除的FAQ条目中复制而来的,因为我发现我应该更好地使用现有的类似问题,而不是仅仅为了回答它们而编造新的“伪问题”。感谢 @Prasoon,他将最后一部分的想法(禁止使用typename/template的情况)编辑到答案中。
1赞 balki 2/2/2011
你能帮我什么时候应该使用这个语法吗?this->模板 f<int>();我收到这个错误“模板”(作为消歧义器)只允许在模板中,但没有模板关键字,它工作正常。
2赞 JorenHeit 1/14/2015
我今天问了一个类似的问题,很快就被标记为重复:stackoverflow.com/questions/27923722/......。我被指示恢复这个问题,而不是创建一个新问题。我必须说我不同意它们是重复的,但我是谁,对吧?那么,即使语法在这一点上不允许除了类型名之外没有其他解释,是否有任何理由强制执行呢?typename
1赞 Johannes Schaub - litb 9/7/2018
@Pablo你没有遗漏任何东西。但仍然需要写消歧义,即使整行不再模棱两可。
3赞 Johannes Schaub - litb 3/30/2019
@L.F. 请创建一个新的 C++20 答案,就像我对 C++11 所做的那样。
165赞 Johannes Schaub - litb 7/11/2013 #3

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_typeintthis->ggAA

当前实例化

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

基于这个概念,如果发现 和 (例如 ) 是当前实例化或其非依赖基类之一的成员(只需立即进行名称查找),则该语言表示 和 (例如 ) 都是当前实例化的成员CurrentInstantiation::FooFooCurrentInstantiationTyped->FooA *a = this; a->Foo

如果限定符是当前实例化的成员,则不再需要关键字 和。这里要记住的一个关键点是,它仍然是一个依赖于类型的名称(毕竟也是依赖于类型的)。但已知是一种类型——编译器会“神奇地”研究这种依赖类型来解决这个问题。typenametemplateA<T>TA<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_typeD::fC

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

需要编译器才能在实例化时捕获错误。因此,您可以获得两个世界中最好的结果:“延迟”查找可以在依赖基类遇到麻烦时保护您,以及“立即”查找,使您免于和 。D<int>::ftypenametemplate

未知专业

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

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

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

在 C++03 中,该语言允许捕获此错误,因为永远不可能有有效的实例化方法(无论您给 )。在 C++11 中,该语言现在有进一步的检查,以便为编译器实现此规则提供更多理由。由于没有依赖基类,并且没有声明任何成员,因此该名称既不是当前实例化的成员,也不是未知专用化的成员。在这种情况下,该代码应该不可能在实例化时有效编译,因此该语言禁止限定符为当前实例化的名称既不是未知专用化的成员,也不是当前实例化的成员(但是,仍然不需要诊断此冲突)。A<T>::hTAAquestionable_typeA<T>::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++ 代码将在实例化时绑定,一切都很好。但是,C++ 会立即将其绑定到,并在实例化时需要仔细检查,检查查找是否仍然匹配。但是,在实例化时,支配规则适用,查找将改为查找。this->fA::fB::fC<A>::gA::f

评论

0赞 Adam Rackis 6/2/2019
仅供参考 - 此处引用了此答案:stackoverflow.com/questions/56411114/...此答案中的许多代码不会在各种编译器上编译。
1赞 Johannes Schaub - litb 6/3/2019
@AdamRackis假设 C++ 规范自 2013 年以来没有改变(我写这个答案的日期),那么你尝试代码的编译器根本就没有实现这个 C++11+ 功能。
34赞 Rapptz 6/7/2014 #4

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


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

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

评论

2赞 diemo 11/8/2021
至少在我看来,这确实是最有效的答案,有清晰的代码示例。它应该在J. Schaub(@litb)进行彻底而详细的解释之前出现。
145赞 Filip Roséen - refp 6/8/2014 #5
前言
这篇文章旨在成为 litb 帖子易于阅读的替代品。

根本目的是相同的;对“什么时候?”和“为什么?”的解释。 并且必须应用。
typenametemplate

和的目的是什么?typenametemplate

typename并且可用于声明模板以外的情况。template

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

我们将此类名称称为,其中解释可能存在歧义;“从属名称”。

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


一个片段说超过 1000 个单词

试着向你自己、朋友或你的猫解释以下函数模板中发生了什么;标有(A)的声明中发生了什么?

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


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

不同的 s 可以极大地改变所涉及的语义。T

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


两种不同的方案

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

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



基本原理

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

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

如果未说明任何内容,则 dependent-name 将被视为变量或函数。



如何处理依赖名称?

如果这是一部好莱坞电影,那么依赖性的名字就是通过身体接触传播的疾病,立即影响到它的宿主,使其感到困惑。混淆,可能导致畸形的 perso-, erhm..程序。

dependent-name 是直接或间接依赖于模板参数的任何名称。

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>
    • F) 末尾的 “type” 依赖于 NestedTrait,而 NestedTrait 依赖于 和;SomeTrait<T>
  • G)
    • “data”,看起来像一个成员函数模板,间接是一个依赖名称,因为 foo 的类型取决于 的实例化。SomeTrait<T>

如果编译器将依赖项名称解释为变量/函数,则语句 (E)、(F) 或 (G) 都无效(如前所述,如果我们不明确说明,就会发生这种情况)。

解决方案

为了使定义有效,我们必须明确地告诉编译器,我们需要 (E) 中的类型、(F) 中的 template-id 和类型,以及 (G) 中的 template-idg_tmpl

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

template但是,在这方面有所不同,因为无法得出这样的结论;“哦,这是一个模板,那么这个东西也必须是一个模板”。这意味着我们直接在我们希望这样对待的任何名称前面申请。template



我可以把关键词贴在任何名字前面吗?

"我可以在任何名称前面粘贴 typenametemplate 吗?我不想担心它们出现的上下文......" - 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

注意:在不需要的上下文中应用 typename模板不被视为良好做法;仅仅因为你可以做某事,并不意味着你应该这样做。


此外,还有一些上下文是明确禁止的:typenametemplate

  • 指定类继承其基时

    在派生类的 base-specifier-list 中写入的每个名称都已被视为类型名称,显式指定既格式不正确,又是多余的。typename

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

  • template-id 是派生类的 using-directive 中引用的 template-id

      struct Base {
        template<class T>
        struct type { };
      };
    
      struct Derived : Base {
        using Base::template type; // ill-formed
        using Base::type;          // legal
      };
    
2赞 KeyC0de 8/11/2018 #6

我逐字逐句地将 JLBorges 对 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++ 模板需要“两阶段名称查找”。最初分析模板时(在进行任何实例化之前),编译器会查找非依赖名称。当发生模板的特定实例化时,模板参数是已知的,编译器会查找依赖项名称。

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


总结

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

12赞 Kai Petzke 4/25/2021 #7

C++20 又名 C++2a

如本提案所述,C++20 / C++2a 进一步放宽了对关键字的要求。特别是,现在在所有这些地方都可以省略,在这些地方,只有一种类型是合法的。因此,如果未知标记必须是类型,C++20 实际上会将其视为类型。但是,为了向后兼容,仍可使用。typenametypenametypename

特别是,大多数 和 声明现在可以在没有 . 也可以在方法返回类型(包括尾随返回类型)的声明、方法和 lambda 参数的声明以及 、 和 的类型参数中省略。usingtypedeftypenametypenamestatic_castconst_castdynamic_castreinterpret_cast

一个值得注意的例外是用户或库定义的模板的实例化的参数列表:即使该特定参数被声明为类型,关键字仍然是必需的。因此,在 C++20 中是合法的,但如果 A 是依赖作用域并期望类型,则格式不正确。typenametypenamestatic_cast<A::B>(arg)my_template_class<A::B>(arg)my_template_class

举几个例子:

class A { public: typedef int type; static const int val { 1 }; };
class B { public: typedef float type; static const int val { 2 }; };
template<typename T> class C {};
template<int I> class D {};
template<typename T> class X {
    T::type v;                                  // OK
    T::type f(T::type arg) { return arg; }      // OK
    T::type g(double arg) { return static_cast<T::type>(arg); } // OK
    // C<T::type> c1;                           // error
    D<T::val> d;                                // OK (as has always been)
    C<typename T::type> c2;                     // OK (old style)
    typedef T::type mytype;                     // OK
    using mytypeagain = T::type;                // OK
    C<mytype> c3;                               // OK (via typedef / using)
};
X<A> xa;
X<B> xb;

评论

0赞 Davis Herring 8/3/2021
作为 C++20 的 DR,解析器指南在相同的上下文中是可选的。template
6赞 Baiyan Huang 5/15/2021 #8

依赖名称是一个依赖于模板参数的名称,我们需要指示编译器以便在实际启动它们之前正确编译模板类/函数。

  • typename ->告诉编译器依赖名称是实际类型

    template <class T>
    struct DependentType
    {
      typename T::type a;
      using Type=typename T::type;
    };
    
    
  • template ->告诉编译器依赖名称是模板函数/类

    template <class T>
    struct DependentTemplate
    {
      // template function
      template <class U>
      static void func() {}
    
      // template class
      template <class U>
      struct ClassName{};
    };
    
    
    template <class T1, class T2>
    void foo()
    {
      // 3 ways to call a dependent template function
      DependentTemplate<T1>::template func<T2>();
      DependentTemplate<T1>().template func<T2>();
      (new DependentTemplate<T1>())->template func<T2>();
    
      // You need both typename and template to reference a dependent template class
      typename DependentTemplate<T1>::template ClassName<T2> obj;
      using Type=typename DependentTemplate<T1>::template ClassName<T2>;
    }
    
-2赞 glades 9/8/2023 #9

简单

当您从模板化类内部调用模板化函数时,需要它:

现场演示

#include <iostream>
#include <string>

struct printable {
    std::string mystr = "Hello World";
    template <typename T>
    auto print() {
        if constexpr (std::same_as<T, std::string>) {
            std::cout << mystr << std::endl;
        }
    }
};


template <typename Printable>
struct entity {
    auto print(Printable& myprintable) {
        myprintable.template print<std::string>();
    }
};

int main() {

    entity<printable> e;
    printable p;

    e.print(p);
}

将输出

Hello World

从 中的模板化函数。print()printable

评论

0赞 HolyBlackCat 9/8/2023
在我看来过于简单了。 仅当调用的函数依赖于模板参数时才需要,并非总是如此。此外,这没有解决.templatetypename
0赞 glades 9/8/2023
@HolyBlackCat我把它简化了,因为在大多数情况下,这就是你面临的情况。
2赞 Jan Schultke 9/28/2023 #10

C++20 和 C++23

对于那些感兴趣的人,我将从现代 C++ 的角度提供完整的答案。 C++20 和 C++23 之间只有细微的区别。

为什么我们需要和?templatetypename

基本问题是,如果没有一些额外的提示,编译器无法理解 C++。从形式上讲,C++ 语法是上下文相关的,您需要提供额外的消歧器。

int x, y;

template <typename T>
void f() {
    T::type * x;
    // if 'type' is a type, then this is declaring 'x' as a pointer to 'T::type'
    // if 'type' is a static data member, this is multiplying 'x' with 'T::type'
    typename T::type* x; // unambiguous

    T::templ<0>> y;
    // if 'templ' is a template, then this is ( (T::templ<0>) > y) )
    // if 'templ' is a static data member, this is ((T::templ < 0) >> y)
    T::template templ<0>> y; // unambiguous
}

正如你所看到的,问题是它可以是任何类型,因此可以是任何东西:静态数据成员、类型别名、模板、成员函数模板、非模板成员函数等。 这是因为 和 是依赖类型,具体取决于 template 参数。TT::typeT::templT::typeT::templT

C++ 通过将 T::something 视为数据成员或有疑问的成员函数来解决这种歧义。仅当已知时,或者指定 或 时,才会被解释为类型或模板。Ttemplatetypenamesomething

依赖类型和表达式

总结一下术语:

  • 诸如 之类的类型依赖于模板参数。T::typeT
  • 如果 或依赖于模板类型参数,则表达式(如 )与类型相关(x + y)xy
  • 如果 或依赖于非类型模板参数,则表达式(如 )与值相关(x + y)xy

通常,当涉及依赖类型时,它就变得必要了。如果依赖类型由依赖表达式(如 )构成,也会发生这种情况。typenametemplatedecltype(x + y)::type

和关键字如何工作?templatetypename

这些关键字有时称为消歧器,并通知编译器您需要类型或模板,而不是数据成员/成员函数。

消歧义器typename

typenametypename说明符前面,并应用于需要消除歧义的名字。例如:

typename T::type // interpreted as type
   │        ▲
   └────────┘

typename T::type::type // interpreted as type
   │              ▲
   └──────────────┘

typename T::templ<0>::type // interpreted as non-type
   │        ▲
   └────────┘

typename T::template templ<0>::type // interpreted as a type
   │                           ▲
   └───────────────────────────┘

消歧义器template

template限定 ID 中充当“胶水”,使其不被解释为 less-than 运算符,而是 template-argument-list 的开头。<

T::templ<0> // '<' is interpreted as less-than

T::template templ<0> // '<0>' is interpreted as template-argument-list

T::template templ<0>::templ<1> // '<1' is interpreted as less than 1

typename T::templ<0> // '<0>' is interpreted as template-argument-list
   │        ▲        // due to type-only context
   └────────┘

如上一个示例所示,可以使不必要,因为如果必须是类型,则只能解释为模板参数列表typenametemplatetempl<templ

还有另一种(现已弃用)的用法,即提供模板-模板参数:template

template <template <typename> typename TT>
void f();

template <typename T>
void g() { f<T::template templ>(); }

这种用法已被弃用,因为在此上下文中,它总是很清楚是模板参数列表,并且毫无意义。<...>template

C++20 和 C++23 的放宽是什么?

许多用途现在已经变得没有必要了。如上所述,有时使不必要。更一般地说,可以在仅类型上下文中省略。templatetypenametypenametemplatetypenametemplate

例如:

using T = T::type;         // OK
static_cast<T::type>(...); // OK
std::is_const_v<T::type>;  // OK
void f(T::type t);         // OK

在 C++20 之前,这些和其他用途的格式是错误的。C++23 添加了 和 之间的交互,从而消除了进一步的使用。 相关建议如下:templatetypename

一般来说,人们会努力消除所有真正需要和不真正必要的地方。 请注意,和并不总是消除歧义所必需的,而只是帮助解析。例如:typenametemplatetemplatetypename

typename T::a::b::c::d::e::f::g::h::i::j x,

在这里,我们仍然需要,尽管这无疑是一个宣言。否则,就需要任意提前查看才能知道这是一种类型并且是有效的。typenameaa::

因此,在这种情况下,它不太可能成为可有可无的。typename