C++ 模版元编程 typename关键字介绍及用法

C++ 模版元编程 typename关键字介绍及用法

编码文章call10242025-02-06 11:02:3870A+A-

一、背景

作为久战一线的牛马近日在看一个rcp 库的源码时,如下:

template 
static typename std::enable_if, Args...)>::type>::value>::type
call_member(const F &f, Self *self, std::weak_ptr ptr,
            std::string &result, std::tuple tp) {
  auto r = call_member_helper(
      f, self, typename nonstd::make_index_sequence{},
      std::move(tp), ptr);
  result = msgpack_codec::pack_args_str(result_code::OK, r);
}

虽说已久经沙场,但看到这几行代码依然感到头皮发麻,完全解释不了typename 在各处的意思,汗颜呐。作为一个有追求的一线牛马,必须搞明白它。上述代码提到的std:make_index_sequence 请看另一篇文章:
https://www.toutiao.com/article/7444487868501819955/

二、typename 是啥?

在C++模板元编程中,typename 是一个非常重要的关键字。typename 的主要用途是告诉编译器,接下来的符号应被解析为一个类型名称

template 
void print_type_size() {
    std::cout << "Size of type: " << sizeof(T) << std::endl;
}

在这段代码中,typename 用于声明模板参数 T 的类型。在C++中,我们常说 “the type of T”(T的类型),这里的 “type”(类型)是指T可以代表的那些可能的类型,如int、std::vector等。也许你会想到上面这段代码中的typename换成class也一样可以,不错!那么这里便有了疑问,

  1. 这两种方式有区别么?
  2. 查看C++ Primer之后,发现两者完全一样。那么为什么C++要同时支持这两种方式呢?
  3. 既然class很早就已经有了,为什么还要引入typename这一关键字呢?

为了区别typename 与 class 在模板编程中的区别我们先来看一个例子:

template 
void foo() {
    T::iterator * iter;// ...
}

这段代码的目的是什么?多数人第一反应可能是和作者想的一样:定义一个指针iter,它指向的类型是包含在类作用域T中的iterator。T::iterator实际上可以是以下三种中的任何一种类型:

  1. 静态数据成员
  2. 静态成员函数
  3. 嵌套类型

前面例子中的ContainsAType::iterator是嵌套类型,完全没有问题。

嵌套类型:

struct ContainsAType {
    struct iterator { /*...*/ };// ...
};

可如果是静态数据成员呢?如果实例化foo模板函数的类型是像这样的:

struct ContainsAnotherType {
    static int iterator;// ...
};

然后如此实例化foo的类型参数:

foo();

那么,T::iterator * iter;被编译器实例化为
ContainsAnotherType::iterator * iter;,这是什么?前面是一个静态成员变量而不是类型,那么这便成了一个乘法表达式,只不过iter在这里没有定义,编译器会报错:

但如果iter是一个全局变量,那么这行代码将完全正确,它是表示计算两数相乘的表达式,返回值被抛弃。

同一行代码能以两种完全不同的方式解释,而且在模板实例化之前,完全没有办法来区分它们,这绝对是滋生各种bug的温床。这时C++标准委员会再也忍不住了,与其到实例化时才能知道到底选择哪种方式来解释以上代码,委员会决定引入一个新的关键字,这就是typename。先不着急下论断,将上述代码作如下修改:

template 
void foo() {
    typename T::iterator* iter;
}

struct ContainsAType {
    struct iterator {

    };
};

编译器就不报错了。为啥呢?先来看看C++ 标准委员会给typename 的定义:

A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.

对于用于模板定义的依赖于模板参数的名称,只有在实例化的参数中存在这个类型名,或者这个名称前使用了typename关键字来修饰,编译器才会将该名称当成是类型。除了以上这两种情况,绝不会被当成是类型。因此,如果你想直接告诉编译器T::iterator是类型而不是变量,只需用typename修饰。这样编译器就可以确定T::iterator是一个类型,而不再需要等到实例化时期才能确定,因此消除了前面提到的歧义。

实际上,这两个关键字在模板参数声明中的语义是一样的。这个设计主要是为了保持对早期C++版本的兼容性,在早期版本中,只有class关键字可以用在这个位置。

既然typename关键字已经存在,而且它也可以用于最常见的指定模板参数,那么为什么不废除class这一用法呢?答案其实也很明显,因为在最终的标准出来之前,所有已存在的书、文章、教学、代码中都是使用的是class,可以想像,如果标准不再支持class,会出现什么情况。

对于指定模板参数这一用法,虽然class和typename都支持,但就个人而言我还是倾向使用typename多一些,因为我始终过不了class表示用户定义类型这道坎。另外,从语义上来说,typename比class表达的更为清楚。C++ Primer也建议使用typename:

使用关键字typename代替关键字class指定模板类型形参也许更为直观,毕竟,可以使用内置类型(非类类型)作为实际的类型形参,而且,typename更清楚地指明后面的名字是一个类型名。但是,关键字typename是作为标准C++的组成部分加入到C++中的,因此旧的程序更有可能只用关键字class。

2.1 typename 与 typedef 区别

  • typedef:在C++中,我们经常使用typedef来创建一个已存在类型的别名(alias):
  • typename:在模板编程中,typename被用来声明模板参数是一个类型:

下面是一个综合代码示例,它展示了typename和typedef在模板编程中的应用:

template 
struct MyContainer {
    typedef T* iterator;  // 使用 typedef 定义了一个迭代器类型别名
    void func() {
        typename T::NestedType t;  // 使用 typename 引用 T 的嵌套类型
        // ...
    }
};

在这个代码中,我们创建了一个名为MyContainer的模板,它定义了一个类型别名iterator(通过typedef),并在成员函数func中使用了typename来引用模板参数T的嵌套类型NestedType。

结论:在英语中,当我们描述这个过程时,我们会说“The typedef keyword is used to create an alias for the type `T*, and the typenamekeyword is used to tell the compiler thatT::NestedType is a type.”(typedef关键字被用来为T*创建一个别名,typename关键字被用来告诉编译器T::NestedType`是一个类型。)

小结:

关键字

描述

用途

示例

typename

声明模板参数是一个类型

模板编程中引用模板类型参数的嵌套类型

typename T::NestedType t;

typedef

创建一个已存在类型的别名

简化复杂的类型声明

typedef int Integer;

三、用法

3.1 typename 在常规场景中使用

3.1.1 在函数参数中使用typename

当我们想要在函数参数中使用依赖类型时,我们需要typename。在这种情况下,typename用于明确指定该类型是从模板参数中派生出来的类型。例如:

template 
void function(typename T::SubType param);

在这个例子中,typename用于指定T::SubType是一个类型。

3.1.2 在函数返回值中使用typename

和函数参数一样,我们也可以在函数的返回值类型中使用typename,用于明确地指出该类型是从模板参数中派生出来的。例如:

template 
typename T::SubType function();

在这个例子中,typename用于指定T::SubType是函数的返回类型。

3.1.3 在类成员中使用typename

typename也可以在类模板中使用,用于指定成员的类型。例如:

template 
class MyClass {
    typename T::SubType member;
};

在这个例子中,typename用于指定T::SubType是类成员member的类型。

3.1.4 小结

下面是一个对于这四种使用 typename 的方式的对比表:

使用场合

示例代码

描述

模板参数列表

template void function(T param);

typename 在此处定义了 T 作为一个类型模板参数。

函数参数

template void function(typename T::SubType param);

typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型。

函数返回值

template typename T::SubType function();

typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型,并且是函数的返回类型。

类成员

template class MyClass { typename T::SubType member; };

typename 在此处明确指定 T::SubType 是从模板参数 T 中派生的类型,并且是类 MyClass 的成员的类型。

3.2. typename的其他使用场景

3.2.1在模板参数列表中使用typename

C++ 中的 typename 关键字在模板编程中有广泛的应用。让我们通过以下小节深入探讨其在不同场景下的用法。

在模板参数列表中,typename关键字用于表示类型模板参数。例如:

template 
void func(T param) {
    // ...
}

在这个示例中,T是一个类型模板参数。我们使用typename关键字来标记它。这样,当我们使用这个模板时,就可以将T替换为任意类型。例如:

func(10);  // T被替换为int
func(20.0);  // T被替换为double

在英语口语交流中,我们会这样叙述上述代码:“我们定义了一个模板函数func,其接收一个模板类型参数T。然后,我们通过调用func(10)和func(20.0),分别将T替换为int和double。”(We defined a template function func that takes a template type parameter T. Then, we instantiated func with T being int and double respectively by calling func(10) and func(20.0).)

3.3 在嵌套类型中使用typename

在模板编程中,我们经常需要处理嵌套类型(Nested Types)。这些类型通常是模板类中定义的类型,或者是模板参数类型的成员类型。在这种情况下,我们需要使用 typename 关键字来显式地告诉编译器,我们正在处理一个类型而不是一个值。例如:

template 
void func() {
    typename T::NestedType object;
    // ...
}

在上述代码中,T::NestedType 是一个嵌套类型,我们使用 typename 关键字来表明这一点。这样,当我们使用模板时,就可以将 T::NestedType 替换为任意嵌套类型。

在英语口语交流中,我们会这样叙述上述代码:“我们定义了一个模板函数 func,其使用 typename 关键字来声明一个嵌套类型 T::NestedType。然后,我们可以在函数体内使用这个嵌套类型。”(We defined a template function func which uses typename keyword to declare a nested type T::NestedType. Then, we can use this nested type inside the function body.)

3.4 typename 在模板特化和偏特化中的应用

模板特化和偏特化是 C++ 模板编程中常见的技巧。当我们需要对某些特定类型提供特殊的处理时,我们可以使用模板特化或偏特化。在这些场景中,我们可能需要使用 typename 关键字。例如,假设我们有一个模板类 MyClass,它有一个嵌套类型 NestedType:

template 
class MyClass {
public:
    typedef T NestedType;
};

我们可以对 MyClass 进行特化,特化时我们需要用到 typename 关键字:

template <>
class MyClass {
public:
    typedef int NestedType;
};

在这个特化版本的 MyClass 中,NestedType 总是为 int 类型。我们使用 typename 关键字来指定 NestedType 的类型。

在下一节中,我们将探讨 typename 在 std::enable_if 中的应用,这是一个非常实用的技巧,可以让我们的模板代码更加灵活和强大。

3.3 typename 在 std::enable_if 中的应用

std::enable_if 是一个常用于模板元编程的工具,它允许我们根据条件启用或禁用模板。在 std::enable_if 中,我们通常需要使用 typename 关键字。例如,假设我们有一个函数模板 func,我们想要只在 T 是整数类型时才使其可用,我们可以这样做:

template 
typename std::enable_if::value>::type
func(T param) {
    // ...
}

在这个示例中,我们使用了 std::is_integral::value 来检查 T 是否为整数类型。如果是,则 std::enable_if 将定义一个名为 type 的类型,否则 std::enable_if 不定义任何类型。由于 typename 关键字的存在,当 T 不是整数类型时,编译器将在编译时期就检测到这个错误,而不会产生错误的运行时行为。现在就好解释文章开头处那个std:enable_if 前的typename 的含义了:

template 
static typename std::enable_if, Args...)>::type>::value>::type
call_member(const F &f, Self *self, std::weak_ptr ptr,
            std::string &result, std::tuple tp) {
  auto r = call_member_helper(
      f, self, typename nonstd::make_index_sequence{},
      std::move(tp), ptr);
  result = msgpack_codec::pack_args_str(result_code::OK, r);
}

3.3 typename的高级用法

在这个部分,我们将深入探讨typename的高级用法,包括多层typename语句,多条typename语句,以及typename与std::enable_if的组合应用。

3.3.1 多层typename语句

有时,我们需要使用多层嵌套的模板类型。在这种情况下,我们可能需要使用多层typename语句。让我们通过一个例子来看看如何使用多层typename语句:

template
class Outer {
public:
    template
    class Inner {};
};
template
void func(typename Outer::template Inner param) {
    // 实现细节...
}

在这个例子中,func函数接收一个类型为Outer::Inner的参数。由于Outer::Inner是一个依赖于模板参数的类型,因此我们需要在前面加上typename关键字。同时,由于Inner是Outer的一个模板成员,所以我们需要在Inner前面加上template关键字。

这样的语法在一开始可能会让人觉得有些困惑,但只要理解了它的原理,就会发现其实并不复杂。关键在于理解这样一个事实:在C++中,模板是一种生成类型的机制。因此,我们需要使用typename和template关键字来告诉编译器,我们正在处理的是类型,而不是值。

3.3.2 多条typename语句

在某些情况下,我们可能需要在一条语句中使用多个typename。例如,我们可能需要在一条函数声明中指定多个模板参数类型。下面是一个例子:

template
class MyClass {
public:
    typedef T type1;
    typedef U type2;
};
template
void func(typename MyClass::type1 param1, typename MyClass::type2 param2) {
    // 实现细节...
}

在这个例子中,func函数接收两个参数,分别是MyClass::type1和MyClass::type2类型。由于这两种类型都依赖于模板参数,所以我们需要在每个类型前面都加上typename关键字。

3.3.3 typename与std::enable_if的组合应用

我们可以将typename与std::enable_if结合起来使用,以实现更复杂的模板特化和条件编

译。std::enable_if是一个模板,它可以根据模板参数的值来决定其是否有type成员类型。下面是一个例子:

template 
typename std::enable_if::value, T>::type
process(T t) {
    std::cout << t << " is an integral number." << std::endl;
}

在这个例子中,process函数只能处理整数类型的参数。当我们试图使用一个非整数类型的参数调用process函数时,会导致编译错误。这是因为在std::enable_if的条件为false时,它没有type成员类型,因此typename std::enable_if::value, T>::type这个类型就不存在,导致process函数无法被声明。

以上就是typename的高级用法。尽管这些用法在一开始可能会让人觉得有些困惑,但只要理解了其原理,就会发现它们其实非常强大,能够帮助我们编写更灵活、更强大的模板代码。

3.3.4 typename的应用:同时使用模板参数列表、函数参数列表和返回值类型进行类型检查

在模板元编程中,我们常常需要进行一些复杂的类型检查。在这个过程中,std::enable_if和typename两者的组合使用提供了一个强大的工具。具体来说,我们可以在模板参数列表、函数参数列表和函数返回值类型中同时使用它们,以进行多个条件的检查。

以下是一个示例,它演示了如何在一个函数中同时使用这三种方式进行类型检查:

// 模板函数,同时使用模板参数列表、函数参数列表和函数返回值类型进行类型检查
template<
        typename T,  // 模板参数 T
        // 在模板参数列表中使用 typename 进行检查,T 必须是整型
        typename std::enable_if::value>::type * = nullptr

// 在返回值类型中使用 typename 进行检查,T 的大小必须大于 4 字节
        typename std::enable_if<(sizeof(T) > 4), T>::type
        process(
            T value,  // 函数参数 value
            // 在函数参数列表中使用 typename 进行检查,T 必须是有符号类型
            typename std::enable_if::value, T>::type * = nullptr
        ) {
    std::cout << value << " is an integral number, its size is greater than 4 bytes, and it is signed." <<
    std::endl;
    return
    value;
}

// 模板类,使用模板参数列表进行类型检查,T 必须是整型
template<
    typename T,  // 模板参数 T
    typename std::enable_if::value>::type * = nullptr

    class MyClass {
    } ;
    

int main() {
    long l = 42;
    process(l);  // 此处可以成功编译和运行,因为 long 是整型,大于 4 字节,且是有符号类型
    // 下面的代码将无法编译,因为 unsigned long 不是有符号类型
    // unsigned long ul = 42;
    // process(ul);
    MyClass myClass;  // 成功实例化 MyClass,因为 int 是整型
    return 0;
}

通过这种方式,我们可以在编译期间进行非常细粒度的类型检查,从而保证我们的代码在运行期间能够按照预期的方式工作。

四. typename语句的限制及常见问题

在深入探索C++的模板元编程中,我们已经了解到typename语句是一个强大且灵活的工具。然而,尽管它有很多优点,但在使用过程中也需要注意一些限制和常见问题。

4.1 不能用于基本数据类型

C++ 的模板元编程规定,typename语句无法应用于基本数据类型。换言之,尝试在int或double这样的基本数据类型之前使用typename语句会导致编译错误。例如:

template 
void function(typename T::subtype var) { // Do something with var. }

在这种情况下,如果你尝试将T替换为int或double(这是没有subtype的基本数据类型),那么编译器就会报错。

4.2 不能用于非类型模板参数

在模板元编程中,有类型和非类型两种模板参数,而typename只能用于类型模板参数。换言之,如果模板参数是非类型的(例如整数,指针,引用等),则不能使用typename关键字。

这是由于typename关键字的设计初衷是为了解决在模板中表示嵌套依赖类型的问题。非类型模板参数不具有嵌套类型,因此在这种情况下使用typename关键字是不合适的。

例如:

template 
class MyClass {
    T data[N];
};

在这个例子中,typename可以用于T,因为T是一个类型模板参数。但是对于N,我们不能使用typename,因为N是一个非类型模板参数。

总的来说,理解和正确使用typename关键字是掌握C++模板元编程的重要步骤。希望通过这个章节,你能够更深入的理解typename语句的一些限制及常见问题。

4.3 小结

最后这个规则看起来有些复杂,可以参考MSDN

  • typename在下面情况下禁止使用:
  • 模板定义之外,即typename只能用于模板的定义中
  • 非限定类型,比如前面介绍过的intvector之类
  • 基类列表中,比如template class C1 : T::InnerType不能在T::InnerType前面加typename
  • 构造函数的初始化列表中
  • 如果类型是依赖于模板参数的限定名,那么在它之前必须加typename(除非是基类列表,或者在类的初始化成员列表中)
  • 其它情况下typename是可选的,也就是说对于一个不是依赖名的限定名,该名称是可选的,例如vector vi;

五、参考文档:

https://developer.aliyun.com/article/1465436

https://feihu.me/blog/2014/the-origin-and-usage-of-typename/

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4