C++之std::type_identity

流星雨爱编程 2024-07-25 17:35:01 阅读 83

目录

1.简介

2.C++20的std::type_identity

3.使用 type_identity

3.1.阻止参数推导

3.1.1.模板参数推导过程中的隐式类型转换

3.1.2.强制显式实例化

3.2.阻止推断指引

3.3.类型保持

3.4.满足一些稀奇古怪的语法

4.示例

5.总结


1.简介

<code>   std::type_identity 是 C++17 引入的一个实用工具,用于确保类型别名保持其引用的完整性。在某些模板元编程的场景中,尤其是在与类型萃取(type traits)和完美转发(perfect forwarding)相关的场景中,保持类型的“原样”传递是非常重要的。

   std::type_identity 是一个简单的模板,它定义了一个别名 type,该别名简单地重新声明了其模板参数类型。但重要的是,它不会修改或“破坏”传递给它的类型。

        在 C++17 之前,要实现这样的效果通常需要一些技巧,比如使用结构体的模板特化或复杂的继承层次结构。但是,std::type_identity 使得这个过程变得更加简单和直观。

        我们用一个例子来说明这个问题,尽管这个例子有点微不足道:

template <class T>

T Add(T a, T b) {

return a + b;

}

Add(4.2, 1); //错误

        尽管将整数与浮点数累加并返回浮点数是 Add 函数应该支持的运算,但是编译器却在这里报错,因为在参数推导的时候,它根据第一个参数推导出 T 是 double 类型,但是根据第二个参数推导出 T 是 int 类型,这产生了矛盾,于是编译器罢工了。

        解决这个问题的方法很简单,比如我们可以谢绝自动推导,用显式实例化(Explicit instantiation)的方式,只是每次调用的时候会麻烦一点:

foo<double>(4.2, 1);

        当然,更常用的实践是借助 C++ 的非推导语境(non-deduced contexts)规避不希望的参数推导,比如下面这种 identity 惯用法:

template< class T >

struct identity {

using type = T;

};

template <class T>

T Add(T a, typename identity<T>::type b) {

return a + b;

}

//相当于 double Add(double a, double b)

foo(4.2, 1); //OK, T 被推导为 double

        根据 identity 的定义,identity<T>::type 其实就是 T,为什么加上这个多此一举的东西就 OK 了?这并不是什么黑魔法,它只是借助了模板参数推导规则中最常用的一种非推导语境,即:

对于模板参数中出现的嵌套类型表达式,域解析运算符(::)左边的嵌套名称说明符如果是个限定性说明符(Qualified identifiers),则该嵌套名称说明符不参与模板参数推导。

        所以用了 identity 大法之后,"::type" 左边的 identity<T> 就不参与模板参数推导,T 就是根据第一个参数推导出的 double,identity<T>::type 就也是 double 了。

2.C++20的std::type_identity

        C++ 20 的 type traits 增加了一个 type_identity,其作用和上一节中自定义的 identity<T> 一样,只是不用重复发明轮子了。直接使用 type_identity 的代码是这个样子的:

template <class T>

T Add(T a, typename std::type_identity<T>::type b) {

return a + b;

}

        C++ 还提供了一个别名:

template< class T >

using type_identity_t = typename type_identity<T>::type;

        使用 type_identity_t<T> 的代码更简单一点:

template <class T>

T Add(T a, std::type_identity_t<T> b) {

return a + b;

}

3.使用 type_identity

3.1.阻止参数推导

3.1.1.模板参数推导过程中的隐式类型转换

        其实建立非推导语境的常用目的是让一个模板参数的类型依赖另一个模板参数的推导结果,这种非推导语境的建立还会带来一些意想不到的效果。比如这个例子:

template <typename ...args_t>

void func(std::function<void(args_t...)> function_, args_t ...args){

/// do something here

}

void func2(std::function<void(int)> function_, int args) {

/// do something here

}

void test() {

func([](int a){ }, 1); //编译错误

func2([](int a){ }, 1); //没有问题

}

        代码中的 func 和 func2 函数的作用相当于一个调用器(Invoker),区别就是 func 是个函数模板,而 func2 是个普通函数。test 函数中调用 func 会导致编译错误,调用 func2 确实正常的。func2 能正常调用说明从 lambda 到 std::function 的隐式转换是没有问题的,那为什么 func 就不能转换呢?

        原因就是在参数推导的时候是不考虑隐式类型转换的,func 是函数模板,它的第一个参数类型 std::function<> 依赖于对模板参数 args_t 的推导结果,推导出来的 std::function<void(args_t...)> 无法与实参传入的 lambda 表达式类型进行匹配,导致推导最终失败,实际上并没有产生一个类似 :

void func(std::function<void(int)> function_, int)

的推导结果,在随后的重载决议时虽然允许隐式转换,但是因为没有一个上述结果能与之进行转换,最终的结果就是编译失败,错误原因是没有一个 func 的实例能匹配 lambda 表达式的调用。func2 能调用成功是因为 func2 是普通函数,此处会进行隐式类型转换。

        此时如果让 std::function<void(args_t...)> 不参与推导,那么它就不需要与实参传入的 lambda 表达式进行匹配,也就是不会导致推导错误,就能得到上面的推导结果,于是在随后的重载决议的时候就能通过隐式类型转换完成函数调用。此时就需要用 type_identity 建立非推导语境了,我们将 func 的设计改成这样就可以了:

template <typename... args_t>

void func(std::type_identity_t<std::function<void(args_t...)>> function_, args_t ...args) {

/// do something here

}

3.1.2.强制显式实例化

        利用 type_identity,还可以在设计上要求用户在使用函数模板的时候必须显式指定模板参数,比如这个例子:

template <typename U, typename V>

void foo(std::type_identity_t<U> u, V v) { ... }

foo<double>(5.9, 6); //编译正确

foo<int>(5.9, 6); // 编译正确,此处发生隐式类型转换

foo(5.3, 6.2); //错误

        foo 函数从设计上强制用户必须指定第一个参数的类型,目的可能是想允许第一个参数的隐式类型转换,也可能是其他目的,总之,使用 type_identity 可以达到这种效果,使用 foo 函数的用户必须显式指定第一个参数的类型。

3.2.阻止推断指引

        C++ 17 引入了一个新的语言特性,就是 CTAD,借助于隐式或显式的推断指引,类模板的模板参数也支持自动推导。但是,隐式的类型推导有可能会产生错误的结果,比如这个 smart_pointer 类的设计:

template <class T>

class smart_pointer {

public:

smart_pointer(T* object);

//...

}

        借助于隐式推断指引,用户可以写出这样的代码,不需要显式指定模板参数 T:

Widget* widget{/* ... */};

smart_pointer ptr{widget};

        但是问题是,T* 代表的指针无法区别 object 是单个对象的指针还是数组,因为数组在函数调用的时候也会退化成指针,所以自动推导出来的类型有可能是错误的,比如这样的代码:

Widget widget[N];

smart_pointer ptr{widget};

        此时推导类型 T 仍然是 Widget,我们希望的是 Widget[]。

        借助于 type_identity,我们可以阻止这种隐式的推断指引,强制用户指定正确的模板参数类型。我们将 smart_pointer 的构造函数修改一下:

smart_pointer(std::type_identity_t<T>* object);

        这样上述代码就会产生错误,用户必须这样使用才能正确编译,这也是我们期望的正确结果:

Widget widget[N];

smart_pointer<Widget[]> ptr{widget};

3.3.类型保持

        type_identity_t<T> 本质上还是 T,所以可以被用在一些需要短暂记忆并保持类型的场合。资料 [5] 是 Timur Doumler 为推动 type_identity 进入标准而做的提案,它给出了几个利用 type_identity 的类型保持功能,使得 type_identity 可以作为其他元函数(Meta function)的实现基础,比如我们可以模仿标准库实现一个 remove_const 的元函数(type traits):

template <typename T>

struct remove_const : std::type_identity<T> {};

template <typename T>

struct remove_const<T const> : std::type_identity<T> {};

大家可能会有疑问,为什么不直接写成这样:

template <typename T>

struct remove_const : T {};

template <typename T>

struct remove_const<T const> : T {};

如果写成第二种形式,则这样的断言会失败:

static_assert(std::is_same_v<remove_const<int const>, int>);

        因为通过 remove_const<T const> 或 remove_const<T> 得到的类型都是 remove_const<T>,不是 T。使用 type_identity,我们就可以借助于它的 type 类型保持得到原始的 T,这样的断言就是成功的:

static_assert(std::is_same_v<remove_const2<int const>::type, int>);

        因为通过 remove_const2<T>::type 可以得到原始类型 T,而不是 remove_const2<T>,那不是我们要的结果。

3.4.满足一些稀奇古怪的语法

        type_identity 的其他用法还包括满足一些稀奇古怪的语法形式,比如语法上我们要创建一个临时数组,但是直接写 'T[]{}' 语法形式上就是错误的,因为编译器不能确定 '[]' 的左边是标识符还是类型。但是用 type_identity 中转一下,编译器就确定知道 '[]' 左边是个类型:

template<typename T>

void Print(T v[]) { ... }

template<typename T>

void Process(T t) {

Print(std::type_identity_t<T[]>{ 1,2,3 }); //编译器能正确理解

//PrintInt(T[]{ 1,2,3 }); //语法错误,[] 左边不能确定是类型

}

4.示例

代码如下:

#include <iostream>

#include <type_traits>

#include <functional>

template <typename T>

struct type_identity

{

using type = T;

};

template <typename T>

using type_identity_t = typename type_identity<T>::type;

template <typename... args_t>

void func_wrapped(type_identity_t<std::function<void(args_t...)>> function_,

args_t ...args)

{

std::cout << "typeid(function_).name(): "

<< typeid(function_).name() << std::endl;

std::cout << "typeid(std::function<void(args_t...)>).name(): "

<< typeid(std::function < void(args_t...)>).name() << std::endl;

std::cout << "std::is_same<>::value "

<< std::is_same< std::function<void(args_t...)>,

type_identity_t<std::function<void(args_t...)>>

>::value << std::endl << std::endl;

// do something here

}

void test()

{

std::cout << __FUNCTION__ << std::endl;

}

int main()

{

std::cout << std::boolalpha;

func_wrapped([](int a) { }, 1);

func_wrapped(test);

return 0;

}

输出:

typeid(function_).name(): St8functionIFviEE

typeid(std::function<void(args_t...)>).name(): St8functionIFviEE

std::is_same<>::value true

typeid(function_).name(): St8functionIFvvEE

typeid(std::function<void(args_t...)>).name(): St8functionIFvvEE

std::is_same<>::value true

5.总结

        希望大家能够有所收获,笔者水平有限。成文之处难免有理解谬误之处,欢迎大家多多讨论,指教。

推荐文章阅读

std::type_identity



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。