目录
前言
本文将从各方面讲解C++11中提出的decltype关键字的用处以及模板函数在同时具有特化及偏特化版本时,编译器对函数调用的选择方法,可以帮助读者更好地认识模板函数的调用以及自动类型推导在模板类型中的妙用。此外,我们还将探讨函数作为函数参数的应用。
一、decltype关键字
当我们需要获取一个表达式的类型时,可以使用decltype
关键字来推断表达式的类型。decltype
关键字在C++11中引入,它可以在编译时获取表达式的类型,包括变量、函数调用、表达式等。
注意上面decltype关键字的作用中有两个关键点:
- 编译时获取表达式的类型
- 编译时获取表达式的类型
因为之前网络有个梗:“请利用模板元编程实现编译期堆排序”
这里就用到了decltype关键字可以在编译时就确定表达式的类型。
前情提要:
类型萃取 type_traits 为C++提供的元编程工具,例如类型特征、编译时有理算术和编译时整数序列。
这里需要先知以下几个模板工具(C++14中利用using起别名简化模板类的使用):
- remove_const_t<类型名> 移除类型中的const修饰符
- remove_reference_t<类型名> 移除类型中的reference修饰符
- decay_t<类型名> 朽化类型(同时移除const和reference)
- is_same_v<T1, T2> 判断T1和T2是否为同类型
接着来看decltype关键字的用法:
decltype会返回表达式的静态类型,包括修饰符
1.获取变量类型
decltype(exp) 这里exp可以是不被()包围的表达式,或是一个类成员访问表达式,或者是一个单独的变量,那么推导类型就和exp一致。
void test1()
{
// decltype(表达式)
// 注意表达式和变量之间的区别 可以给变量加括号使得强行编译为表达式
//1. 获取变量的类型:
constexpr int num1 = 10;
decltype(num1) num2 = 92; // decltype(变量) 推断时类型确定以num1变量类型为标准
cout << is_same_v<const int, decltype(num2)> << endl; // 1 推断出num2类型为const int
cout << is_same_v<int, decltype(num2)> << endl; // 0
cout << is_same_v<int, remove_const_t<decltype(num2)>> << endl; // 1
cout << is_same_v<int, decay_t<decltype(num2)>> << endl; // 1
}
2.获取表达式类型
decltype(exp) 这里exp如果是一个左值,或者被()包围,那么推导类型就是exp的引用。
//2. 获取表达式的类型:
constexpr int num11 = 10;
decltype((num11)) num22 = num11; // (num22)可以让编译器认为这是表达式,不是变量
cout << is_same_v<const int, decltype(num22)> << endl; // 0
cout << is_same_v<int&, decltype(num22)> << endl; // 0
cout << is_same_v<const int&, decltype(num22)> << endl; // 1 推断出num22类型为const int&
// **********************************
// 注意这里remove_const_t修改的const修饰符作用在了num22引用指向的对象上 对num22的类型推导仍为const int&
cout << is_same_v<const int&, remove_const_t<decltype(&num22)>> << endl; // 1
cout << is_same_v<int&, remove_const_t<decltype(num22)>> << endl; // 0
// **********************************
cout << is_same_v<const int, remove_reference_t<decltype(num22)>> << endl; // 1
cout << is_same_v<int, decay_t<decltype(num22)>> << endl; // 1
3.获取表达式(函数)类型
decltype(exp) 这里exp如果是函数调用,那么推导类型就和函数返回值类型一致。
首先给出自动推导返回值类型的函数:
decltype(auto) get_id() // 自动推导可以保留引用
{
const string s = "s_t_r_i_n_g";
return s;
}
调用接收:
//3. 获取函数返回值的类型:
decltype(get_id()) id;
调试窗口:
不要在意变量id的内容为空,因为你没有给他初始化赋值,但是可以看到id的类型与函数返回值类型一致为string,推导正确。
4.用于代理模式中完美转发函数返回值类型
首先给出代理模式的简单实现模板:
template<typename T>
class RealObject
{
public:
RealObject(){}
RealObject(T val):m_val(val){}
void setValue(T val)
{
m_val = val;
}
T getValue()
{
return m_val;
}
private:
T m_val;
};
template<typename... T>
class ProxyObject
{
private:
RealObject<T...> realobject;
public:
ProxyObject(T... val) { realobject.setValue(forward<T...>(val)...); }
template<typename... Arg>
decltype(auto) getValue(Arg&... arg) // 利用decltype(auto)可以方便实现返回值的自我推导
{
return realobject.getValue();
}
};
给出实例化并利用auto变量自动推导接收函数返回值的代码:
//4. 代理模式中用于完美转发函数返回值:
const vector<int> v{1, 2, 3, 4, 5};
ProxyObject proObj(v); // 创建代理对象
const auto val = proObj.getValue();
cout << "value = " << val << endl;
由于这里需要直接cout输出vector对象,所以我们需要对应vector容器全局重载左移运算符:
template<typename T>
ostream& operator<<(ostream& os,const vector<T>& v)
{
os << '{';
for(size_t i= 0;i<v.size();++i)
{
os << v.at(i);
if (i != v.size() - 1) { os << ", "; }
}
os << '}';
return os;
}
运行结果:
可以看到代理模式可以用于保护被代理对象,限制对其敏感操作的访问,只提供有必要的接口,同时利用 decltype(auto) 成功实现了代码复用,以实现多类型的函数返回值要求。
二、模板的偏特化与全特化
首先给出模板函数及其偏特化、全特化后的函数:
template<typename T,typename N>
void print()
{
cout << "模板类型" << endl;
}
template<typename T,int N = 0>
void print()
{
cout << "偏特化" << endl;
}
template<char C = 'c', int Size = 0>
void print()
{
cout << "全特化" << endl;
}
接着给出调用测试部分代码:
void test2()
{
int* p = new int[10];
int size1 = _msize(p)/sizeof(p[0]);
cout << "size1 = " << size1 << endl;
//const int size2 = sizeof(p) / sizeof(p[0]); // 动态数组错误大小求法
//cout << "size2 = " << size2 << endl;
constexpr double b = 10.8;
print<char, decltype(b)>();
print<char, decltype(size1)>();
print<char>();
print<char, 100>();
//print<int, size1>(); size1的值在运行时才能确定,所以该语句编译报错
print<>();
print();
print<'w', 8>();
delete[] p;
}
运行结果:
可以看到匹配全特化和偏特化函数的模板指定类型同样可以调用普通函数模板,但是优先调用了全特化、偏特化函数,固然存在一种优先调用规则来决定函数的调用选择。
优先调用规则如下:
- 如果存在完全匹配的全特化,将优先选择全特化进行调用。
- 如果不存在完全匹配的全特化,将选择最匹配的偏特化进行调用。
- 如果存在多个偏特化同样匹配,或者没有偏特化,将选择最匹配的普通函数模板进行调用。
三、函数也能做函数参数
首先给出打印数字的函数:
void printNum(const int num)
{
cout << num << endl;
}
接着给出基于模板元编程的利用函数做函数参数的代码及调用实例:
template<typename... T>
void do_N_case(auto func(T...), const int N)
{
for(int i = 0;i<N;++i)
{
func(i);
}
}
void test3()
{
do_N_case<int>(printNum, 8);
}
运行结果:
该模块可以结合lambda表达式灵活应用,具体拓展等方面的知识点在后面文章会详细进行解释。
总结
通过本篇博客的学习,我们深入了解了decltype关键字的用法和应用场景,以及模板的偏特化与全特化的灵活性。我们还学会了如何将函数作为函数参数,从而实现更灵活的代码设计和调用方式。这些知识和技巧将有助于我们在C++编程中更好地应对复杂的类型推导和定制化处理的需求。希望本篇博客能够对读者有所帮助,提供了一些有用的技术和思路。如果您对这些内容有任何疑问或者需要进一步的探讨,请随时提问。