1276

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++ 对此感到窒息是完全正确的。
我也知道我应该能够添加template某个地方来告诉编译器 inUnion 是一个模板 ID。但具体在哪里?然后它应该假设 inUnion 是一个类模板,即inUnion<U>命名一个类型而不是一个函数吗?

4

8 回答 8

1311

(请参阅此处了解我的 C++11 答案

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

t * f;

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

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

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

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

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

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

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

“类型名”关键字

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

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

有许多名称typename是不必要的,因为编译器可以通过模板定义中适用的名称查找来确定如何解析构造本身 - 例如使用T *f;, whenT是类型模板参数。但是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只允许在限定名称之前- 因此,如果它们这样做,则始终知道非限定名称引用类型是理所当然的。

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

“模板”关键字

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

boost::function< int() > f;

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

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

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

在名称查找 (3.4) 发现一个名称是一个模板名称后,如果这个名称后跟一个 <,则 < 总是被视为模板参数列表的开头,而不是一个名称后跟 less-比运营商。

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

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

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

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)、operator-function-id (13.5)、conversion-function-id (12.3.2) 或 template-id (14.2) 的使用,表示实体或标签 (6.6.4, 6.1)

标识符只是一个简单的字符/数字序列,而接下来的两个是operator +andoperator 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不必总是出现在名称的最后部分。它可以出现在用作作用域的类名之前的中间,如下例所示

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

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

  • 在依赖基类的名字上你是不允许写的typename。假定给定的名称是类类型名称。对于基类列表和构造函数初始化列表中的名称都是如此:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
    
  • 在 using-declarations 中,不能template在 last 之后使用::,并且 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
     };
    
于 2009-03-05T00:27:57.213 回答
155

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某处显式专门化,那也会不会影响该模板中的代码,因此其含义不会受到以后的特殊化A!) 的影响。

当前实例化

为了改善这种情况,在 C++11 中,语言会跟踪类型何时引用封闭模板。要知道,该类型必须是使用某种形式的名称形成的,也就是它自己的名称(在上面,A, A<T>, ::A<T>)。由这种名称引用的类型称为当前实例化。如果形成名称的类型是成员/嵌套类(然后,A::NestedClass并且A都是当前实例化),则可能有多种类型都是当前实例化。

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

如果限定符是当前实例化的成员,则不再需要关键字typenameand 。template这里要记住的一个关键点是它A<T>仍然一个依赖于类型的名称(毕竟T也是依赖于类型的)。But A<T>::result_typeis known to be a 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。因此,您将获得两全其美:“延迟”查找在您遇到依赖基类问题时保护您,以及“立即”查找让您从typenametemplate.

未知专业

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

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

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

在 C++03 中,该语言允许捕获此错误,因为永远不会有有效的方法来实例化A<T>::h(无论您给出什么参数T)。在 C++11 中,该语言现在有进一步的检查,以便为编译器提供更多实现此规则的理由。由于A没有依赖的基类,并且A没有声明 member questionable_type,因此该名称A<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++03 代码将在实例化时绑定this->fA::f一切都很好。然而,C++11 立即将其绑定到B::f并在实例化时需要仔细检查,检查查找是否仍然匹配。然而,当实例化时C<A>::gDominance Rule适用并且查找会A::f代替。

于 2013-07-10T20:02:50.770 回答
119

前言

这篇文章旨在成为litb 文章的易于阅读的替代品。

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


typename和的目的是template什么?

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

C++中的某些上下文中,必须明确告诉编译器如何处理名称,所有这些上下文都有一个共同点;它们至少依赖于一个template-parameter

我们将此类名称称为解释中可能存在歧义的名称,例如:“从属名称”。

这篇文章将解释dependent-names和两个关键字之间的关系。


一个片段说超过 1000 个单词

尝试向您自己、朋友或您的猫解释以下函数模板中发生的事情;标记为 ( A )的语句中发生了什么?

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


它可能不像人们想象的那么容易,更具体地说,评估 ( 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 ) 中,我们将声明一个名为x的指向 int 的指针,但是;

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



基本原理

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

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

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



如何处理依赖名称?

如果这是一部好莱坞电影,从属名字将是通过身体接触传播的疾病,立即影响其宿主使其混乱。可能会导致不正确的人,erhm..程序的混乱。

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

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 )
    • "NestedTrait"是一个template-id,取决于SomeTrait<T>, 和;
    • ( F )末尾的“类型”取决于NestedTrait,它取决于SomeTrait<T>, 和;
  • G )
    • "data"看起来像一个成员函数模板,间接地是一个从属名称,因为foo的类型取决于SomeTrait<T>.

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

解决方案

为了g_tmpl有一个有效的定义,我们必须明确地告诉编译器我们期望一个类型在(E)中,一个模板ID和一个类型在(F)中,一个模板ID在(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
}

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

template然而,在这方面是不同的,因为没有办法得出这样的结论;“哦,这是一个模板,那么这个其他的东西也必须是一个模板”。这意味着我们直接在我们想要处理的任何名称template前面应用。



我可以将关键字放在任何名称的前面吗?

我可以坚持在任何名字typename前面template吗?我不想担心它们出现的上下文...... ” -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

注意:应用typenametemplate在不需要的情况下不被视为良好做法;仅仅因为您可以做某事,并不意味着您应该做某事。


此外,在某些情况下typenametemplate明确禁止:

  • 指定类继承的基时

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

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

  • 模板 ID是派生类的using 指令中引用的模板 ID 时

      struct Base {
        template<class T>
        struct type { };
      };
    
      struct Derived : Base {
        using Base::template type; // ill-formed
        using Base::type;          // legal
      };
    
于 2014-06-07T20:28:32.803 回答
29

该答案旨在回答(部分)标题问题的简短而甜蜜的答案。如果您想要更详细的答案来解释为什么必须将它们放在那里,请转到此处


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

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.

于 2014-06-06T22:23:21.637 回答
20
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:看看typelists,尤其是在 Andrei Alexandrescu 的书:Modern C++ Design

于 2009-03-04T13:37:47.963 回答
4

C++20 又名 C++2a

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

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

一个值得注意的例外,typename仍然需要,是在用户或库定义模板的实例化参数列表中:即使该特定参数被声明为类型,typename关键字仍然是必需的。所以static_cast<A::B>(arg)在 C++20 中是合法的,但是my_template_class<A::B>(arg)如果 A 是一个依赖范围并且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;
于 2021-04-25T07:23:32.900 回答
2

我将 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,前提是您具有引用类型并依赖于模板参数的限定名称。

于 2018-08-11T12:20:50.773 回答
1

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

  • 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>;
    }
    
于 2021-05-15T00:07:02.690 回答