自己怎么手机做网站,wordpress 调用文章,全国信用信息公示系统官网,模版网站做支付功能【Effective Modern C】第1章 型别推导 文章目录 【Effective Modern C】第1章 型别推导条款1#xff1a;理解模板型别推导基础概念模板型别推导的三种情况情景一 ParamType 是一个指针或者引用#xff0c;但非通用引用情景二 ParamType是一个通过引用情景三 ParamType既不是…【Effective Modern C】第1章 型别推导 文章目录 【Effective Modern C】第1章 型别推导条款1理解模板型别推导基础概念模板型别推导的三种情况情景一 ParamType 是一个指针或者引用但非通用引用情景二 ParamType是一个通过引用情景三 ParamType既不是指针也不是引用时 数组实参和函数实参的型别推导数组实参函数实参 要点 条款2理解auto型别推导一般情景特例auto推导函数返回值和lambda形参要点 条款3理解decltype要点 条款4掌握查看型别推导结果的方法IDE编辑器编译器诊断运行时输出要点 条款1理解模板型别推导
在现代C编程中模板已经成为不可或缺的一部分。模板使得我们可以编写出更为通用和灵活的代码。然而模板型别推导的复杂性也常常令开发者感到困惑。本文将总结其核心内容并结合实际例子帮助读者更好地理解模板型别推导。
基础概念
模板型别推导的基本机制是编译器根据传入的实参推导出模板参数的具体类型。C模板分为函数模板和类模板这里主要讨论函数模板的型别推导。
templatetypename T
void f(T param);而一次调用形如
f(expr) // 以某表达式调用f在编译期间编译器使用 expr 进行两个类型推导一个是针对 T 的另一个是针对ParamType 的。这两个类型通常是不同的因为 ParamType 包含一些修饰比如 const 和引用修饰符。举个例子如果模板这样声明
templatetypename T
void f(const T param); //ParamType是const T当调用函数模板 f 时编译器会尝试推导参数 T 的类型。例如
int x 0;
f(x); //T 被推导为 int ParamType 却被推导为 const int模板型别推导的三种情况
T 的类型推导不仅取决于 expr 的类型也取决于 ParamType 的类型。这里有三种情况
情景一 ParamType 是一个指针或者引用但非通用引用
在这种情况下类型推导会这样进行
如果expr的类型是一个引用忽略引用部分然后expr的类型与ParamType进行模式匹配来决定T
举个例子如果这是我们的模板
templatetypename T
void f(T param); //param是一个引用我们声明这些变量
int x27; //x是int
const int cxx; //cx是const int
const int rxx; //rx是指向作为const int的x的引用在不同的调用中对param和T推导的类型会是这样
f(x); //T是intparam的类型是int
f(cx); //T是const intparam的类型是const int
f(rx); //T是const intparam的类型是const int如果param是一个指针或者指向const的指针而不是引用情况本质和上面也一样。
情景二 ParamType是一个通过引用
这样的形参被声明为像右值引用一样也就是在函数模板中假设有一个类型形参T那么通用引用声明形式就是T)。推导规则
如果expr是左值T和ParamType都会被推导为左值引用。这非常不寻常第一这是模板类型推导中唯一一种T被推导为引用的情况。第二虽然ParamType被声明为右值引用类型但是最后推导的结果是左值引用。如果expr是右值就使用正常的也就是情景一推导规则
举个例子
templatetypename T
void f(T param); //param现在是一个通用引用类型int x27; //如之前一样
const int cxx; //如之前一样
const int rxcx; //如之前一样f(x); //x是左值所以T是int//param类型也是intf(cx); //cx是左值所以T是const int//param类型也是const intf(rx); //rx是左值所以T是const int//param类型也是const intf(27); //27是右值所以T是int//param类型就是int情景三 ParamType既不是指针也不是引用时
我们通过传值pass-by-value的方式处理
templatetypename T
void f(T param); //以传值的方式处理param这意味着无论传递什么param都会成为它的一份拷贝——一个完整的新对象。事实上param成为一个新对象这一行为会影响T如何从expr中推导出结果。
和之前一样如果expr的类型是一个引用忽略这个引用部分如果忽略expr的引用性reference-ness之后expr是一个const那就再忽略const。如果它是volatile也忽略volatile
因此
int x27; //如之前一样
const int cxx; //如之前一样
const int rxcx; //如之前一样f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是intparam是一个完全独立于cx和rx的对象——是cx或rx的一个拷贝。具有常量性的cx和rx不可修改并不代表param也是一样。
认识到只有在传值给形参时才会忽略const和volatile这一点很重要正如我们看到的对于reference-to-const和pointer-to-const形参来说expr的常量性constness在推导时会被保留。但是考虑这样的情况expr是一个const指针指向const对象expr通过传值传递给param
templatetypename T
void f(T param); //仍然以传值的方式处理paramconst char* const ptr //ptr是一个常量指针指向常量对象 Fun with pointers;f(ptr); //传递const char * const类型的实参在这里稍微有点复杂解引用符号*的右边的const表示ptr本身是一个constptr不能被修改为指向其它地址也不能被设置为null解引用符号左边的const表示ptr指向一个字符串这个字符串是const因此字符串不能被修改。当ptr作为实参传给f组成这个指针的每一比特都被拷贝进param。像这种情况ptr自身的值会被传给形参根据类型推导的第三条规则ptr自身的常量性constness将会被省略所以param是const char*也就是一个可变指针指向const字符串。在类型推导中这个指针指向的数据的常量性const将会被保留但是当拷贝ptr来创造一个新指针param时ptr自身的常量性const将会被忽略。
数组实参和函数实参的型别推导
上面的内容几乎覆盖了模板类型推导的大部分内容但这里还有一些小细节值得注意。在模板类型推导时数组名或者函数名实参会退化为指针除非它们被用于初始化引用。
数组实参
数组类型不同于指针类型虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是在很多上下文中数组会退化为指向它的第一个元素的指针。这样的退化允许像这样的代码可以被编译
const char name[] J. P. Briggs; //name的类型是const char[13]const char * ptrToName name; //数组退化为指针在这里const char*指针ptrToName会由name初始化而name的类型为const char[13]这两种类型const char*和const char[13]是不一样的但是由于数组退化为指针的规则编译器允许这样的代码。
但是现在难题来了虽然函数不能声明形参为真正的数组但是可以接受指向数组的引用所以我们修改f为传引用
templatetypename T
void f(T param); //传引用形参的模板我们这样进行调用
f(name); //传数组给fT被推导为了真正的数组这个类型包括了数组的大小在这个例子中T被推导为const char[13]f的形参该数组的引用的类型则为const char ()[13]。是的这种语法看起来又臭又长但是知道它将会让你在关心这些问题的人的提问中获得大神的称号。
有趣的是可声明指向数组的引用的能力使得我们可以创建一个模板函数来推导出数组的大小
//在编译期间返回一个数组大小的常量值//数组形参没有名字
//因为我们只关心数组的大小
templatetypename T, std::size_t N //关于
constexpr std::size_t arraySize(T ()[N]) noexcept //constexpr
{ //和noexceptreturn N; //的信息
} //请看下面函数实参
对于数组类型推导的全部讨论都可以应用到函数类型推导和退化为函数指针上来。结果是
void someFunc(int, double); //someFunc是一个函数//类型是void(int, double)templatetypename T
void f1(T param); //传值给f1templatetypename T
void f2(T param); //传引用给f2f1(someFunc); //param被推导为指向函数的指针//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用//类型是void()(int, double)要点
在模板类型推导时有引用的实参会被视为无引用他们的引用会被忽略对于通用引用的推导左值实参会被特殊对待对于传值类型推导const和/或volatile实参会被认为是non-const的和non-volatile的在模板类型推导时数组名或者函数名实参会退化为指针除非它们被用于初始化引用
条款2理解auto型别推导
auto类型推导就是模板型别推导除了一个奇妙的特例除外。
一般情景
当一个变量使用auto进行声明时auto扮演了模板中T的角色变量的类型说明符扮演了ParamType的角色。废话少说这里便是更直观的代码描述考虑这个例子
auto x 27;这里x的类型说明符是auto自己另一方面在这个声明中
const auto cx x;类型说明符是const auto。另一个
const auto rx x;类型说明符是const auto。在这里例子中要推导xcx和rx的类型编译器的行为看起来就像是认为这里每个声明都有一个模板然后使用合适的初始化表达式进行调用对比上面的auto
templatetypename T //概念化的模板用来推导x的类型
void func_for_x(T param);func_for_x(27); //概念化调用//param的推导类型是x的类型templatetypename T //概念化的模板用来推导cx的类型
void func_for_cx(const T param);func_for_cx(x); //概念化调用//param的推导类型是cx的类型templatetypename T //概念化的模板用来推导rx的类型
void func_for_rx(const T param);func_for_rx(x); //概念化调用//param的推导类型是rx的类型正如我说的auto类型推导除了一个例外我们很快就会讨论其他情况都和模板类型推导一样。
条款一基于ParamType——在函数模板中param的类型说明符把模板类型推导分成三个部分来讨论。在使用auto作为类型说明符的变量声明中类型说明符代替了ParamType因此Item1描述的三个情景稍作修改就能适用于auto
情景一类型说明符是一个指针或引用但不是通用引用情景二类型说明符一个通用引用情景三类型说明符既不是指针也不是引用
我们早已看过情景一和情景三的例子
auto x 27; //情景三x既不是指针也不是引用
const auto cx x; //情景三cx也一样
const auto rxcx; //情景一rx是非通用引用情景二像你期待的一样运作
auto uref1 x; //x是int左值//所以uref1类型为int
auto uref2 cx; //cx是const int左值//所以uref2类型为const int
auto uref3 27; //27是int右值//所以uref3类型为int条款一讨论并总结了对于non-reference类型说明符数组和函数名如何退化为指针。那些内容也同样适用于auto类型推导
const char name[] //name的类型是const char[13]R. N. Briggs;auto arr1 name; //arr1的类型是const char*
auto arr2 name; //arr2的类型是const char ()[13]void someFunc(int, double); //someFunc是一个函数//类型为void(int, double)auto func1 someFunc; //func1的类型是void (*)(int, double)
auto func2 someFunc; //func2的类型是void ()(int, double)就像你看到的那样auto类型推导和模板类型推导几乎一样的工作它们就像一个硬币的两面。
特例
开始提到的与模板型别推导不同的一个特例C11的统一初始化uniform initialization的语法
int x1 27; // C98
int x2(27); // C98
int x3 { 27 };
int x4{ 27 };但是条款5解释了使用auto说明符代替指定类型说明符的好处所以我们应该很乐意把上面声明中的int替换为auto我们会得到这样的代码
auto x1 27;
auto x2(27);
auto x3 { 27 };
auto x4{ 27 };这些声明都能通过编译但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为int值为27的变量但是后面两个声明了一个存储一个元素27的 std::initializer_listint类型的变量。
auto x1 27; //类型是int值是27
auto x2(27); //同上
auto x3 { 27 }; //类型是std::initializer_listint//值是{ 27 }
auto x4{ 27 }; //同上这就造成了auto类型推导不同于模板类型推导的特殊情况。当用auto声明的变量使用花括号进行初始化auto类型推导推出的类型则为std::initializer_list。如果这样的一个类型不能被成功推导比如花括号里面包含的是不同类型的变量编译器会拒绝这样的代码
auto x5 { 1, 2, 3.0 }; //错误无法推导std::initializer_listT中的T就像注释说的那样在这种情况下类型推导将会失败但是对我们来说认识到这里确实发生了两种类型推导是很重要的。一种是由于auto的使用x5的类型不得不被推导。因为x5使用花括号的方式进行初始化x5必须被推导为std::initializer_list。但是std::initializer_list是一个模板。std::initializer_listT会被某种类型T实例化所以这意味着T也会被推导。 推导落入了这里发生的第二种类型推导——模板类型推导的范围。在这个例子中推导之所以失败是因为在花括号中的值并不是同一种类型。
对于花括号的处理是auto类型推导和模板类型推导唯一不同的地方在于auto会假定用大括号括起的初始化表达式代表一个std::initializer_list但模板型别推导却不会
auto x { 11, 23, 9 }; //x的类型是std::initializer_listinttemplatetypename T //带有与x的声明等价的
void f(T param); //形参声明的模板f({ 11, 23, 9 }); //错误不能推导出T然而如果在模板中指定T是std::initializer_listT而留下未知T,模板类型推导就能正常工作
templatetypename T
void f(std::initializer_listT initList);f({ 11, 23, 9 }); //T被推导为intinitList的类型为//std::initializer_listint一个非常重要的点就是如果你想要拥抱统一初始化的哲学——就是说会自然而然吧初始化值扩在大括号里面的话那么务必请牢记这条规则。
auto推导函数返回值和lambda形参
对于C11故事已经说完了。但是对于C14故事还在继续C14允许auto用于函数返回值并会被推导而且C14的lambda函数也允许在形参声明中使用auto。但是在这些情况下auto实际上使用模板类型推导的那一套规则在工作而不是auto类型推导所以说下面这样的代码不会通过编译
auto createInitList()
{return { 1, 2, 3 }; //错误不能推导{ 1, 2, 3 }的类型
}同样在C14的lambda函数中这样使用auto也不能通过编译
std::vectorint v;
…
auto resetV [v](const auto newValue){ v newValue; }; //C14
…
resetV({ 1, 2, 3 }); //错误不能推导{ 1, 2, 3 }的类型要点
auto类型推导通常和模板类型推导相同但是auto类型推导假定花括号初始化代表std::initializer_list而模板类型推导不这样做在C14中auto允许出现在函数返回值或者lambda函数形参中但是它的工作机制是模板类型推导那一套方案而不是auto类型推导
条款3理解decltype
decltype是一个奇怪的东西。给它一个名字或者表达式decltype就会告诉你这个名字或者表达式的类型。通常它会精确的告诉你你想要的结果。但有时候它得出的结果也会让你挠头半天。
先从一般案例开始
const int i 0; //decltype(i)是const intbool f(const Widget w); //decltype(w)是const Widget//decltype(f)是bool(const Widget)struct Point{int x,y; //decltype(Point::x)是int
}; //decltype(Point::y)是intWidget w; //decltype(w)是Widgetif (f(w))… //decltype(f(w))是booltemplatetypename T //std::vector的简化版本
class vector{
public:…T operator[](std::size_t index);…
};vectorint v; //decltype(v)是vectorint
…
if (v[0] 0)… //decltype(v[0])是int在C11中decltype最主要的用途就是用于声明函数模板而这个函数返回类型依赖于形参类型。举个例子假定我们写一个函数一个形参为容器一个形参为索引值这个函数支持使用方括号的方式也就是使用“[]”访问容器中指定索引值的数据然后在返回索引操作的结果前执行认证用户操作。函数的返回类型应该和索引操作返回的类型相同。
对一个T类型的容器使用operator[] 通常会返回一个T对象比如std::deque就是这样。但是std::vector有一个例外对于std::vectorbooloperator[]不会返回bool它会返回一个全新的对象。关于这个问题的详细讨论请参见条款6这里重要的是我们可以看到对一个容器进行operator[]操作返回的类型取决于容器本身。
使用decltype使得我们很容易去实现它这是我们写的第一个版本使用decltype计算返回类型这个模板需要改良我们把这个推迟到后面
templatetypename Container, typename Index //可以工作
auto authAndAccess(Container c, Index i) //但是需要改良-decltype(c[i])
{authenticateUser();return c[i];
}函数名称前面的auto不会做任何的类型推导工作。相反的他只是暗示使用了C11的尾置返回类型语法即在函数形参列表后面使用一个”-“符号指出函数的返回类型尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。在authAndAccess函数中我们使用c和i指定返回类型。如果我们按照传统语法把函数返回类型放在函数名称之前c和i就未被声明所以不能使用。
在这种声明中authAndAccess函数返回operator[]应用到容器中返回的对象的类型这也正是我们期望的结果。
C11允许自动推导单一语句的lambda表达式的返回类型 C14扩展到允许自动推导所有的lambda表达式和函数甚至它们内含多条语句。对于authAndAccess来说这意味着在C14标准下我们可以忽略尾置返回类型只留下一个auto。使用这种声明形式auto标示这里会发生类型推导。更准确的说编译器将会从函数实现中推导出函数的返回类型。
templatetypename Container, typename Index //C14版本
auto authAndAccess(Container c, Index i) //不那么正确
{authenticateUser();return c[i]; //从c[i]中推导返回类型
}条款2解释了函数返回类型中使用auto编译器实际上是使用的模板类型推导的那套规则。如果那样的话这里就会有一些问题。正如我们之前讨论的operator[]对于大多数T类型的容器会返回一个T但是条款1解释了在模板类型推导期间表达式的引用性reference-ness会被忽略。基于这样的规则考虑它会对下面用户的代码有哪些影响
std::dequeint d;
…
authAndAccess(d, 5) 10; //认证用户返回d[5]//然后把10赋值给它//无法通过编译器在这里d[5]本该返回一个int但是模板类型推导会剥去引用的部分因此产生了int返回类型。函数返回的那个int是一个右值上面的代码尝试把10赋值给右值intC11禁止这样做所以代码无法编译。
要想让authAndAccess像我们期待的那样工作我们需要使用decltype类型推导来推导它的返回值即指定authAndAccess应该返回一个和c[i]表达式类型一样的类型。C期望在某些情况下当类型被暗示时需要使用decltype类型推导的规则C14通过使用decltype(auto)说明符使得这成为可能。我们第一次看见decltype(auto)可能觉得非常的矛盾到底是decltype还是auto实际上我们可以这样解释它的意义auto说明符表示这个类型将会被推导decltype说明decltype的规则将会被用到这个推导过程中。因此我们可以这样写authAndAccess
templatetypename Container, typename Index //C14版本
decltype(auto) //可以工作
authAndAccess(Container c, Index i) //但是还需要
{ //改良authenticateUser();return c[i];
}现在authAndAccess将会真正的返回c[i]的类型。现在事情解决了一般情况下c[i]返回TauthAndAccess也会返回T特殊情况下c[i]返回一个对象authAndAccess也会返回一个对象。
decltype(auto)的使用不仅仅局限于函数返回类型当你想对初始化表达式使用decltype推导的规则你也可以使用
Widget w;const Widget cw w;auto myWidget1 cw; //auto类型推导//myWidget1的类型为Widget
decltype(auto) myWidget2 cw; //decltype类型推导//myWidget2的类型是const Widget但是这里有两个问题困惑着你。一个是之前提到的authAndAccess的改良至今都没有描述。现在说一说这个问题。
再看看C14版本的authAndAccess声明
templatetypename Container, typename Index
decltype(auto) authAndAccess(Container c, Index i);容器通过传引用的方式传递非常量左值引用lvalue-reference-to-non-const因为返回一个引用允许用户可以修改容器。但是这意味着在不能给这个函数传递右值容器右值不能被绑定到左值引用上除非这个左值引用是一个constlvalue-references-to-const但是这里明显不是。
公认的向authAndAccess传递一个右值是一个edge case译注在极限操作情况下会发生的事情类似于会发生但是概率较小的事情。一个右值容器是一个临时对象通常会在authAndAccess调用结束被销毁这意味着authAndAccess返回的引用将会成为一个悬置的dangle引用。但是使用向authAndAccess传递一个临时变量也并不是没有意义有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝比如这样
std::dequestd::string makeStringDeque(); //工厂函数//从makeStringDeque中获得第五个元素的拷贝并返回
auto s authAndAccess(makeStringDeque(), 5);要想支持这样使用authAndAccess我们就得修改一下当前的声明使得它支持左值和右值。重载是一个不错的选择一个函数重载声明为左值引用另一个声明为右值引用但是我们就不得不维护两个重载函数。另一个方法是使authAndAccess的引用可以绑定左值和右值Item24解释了那正是通用引用能做的所以我们这里可以使用通用引用进行声明
templatetypename Containter, typename Index //现在c是通用引用
decltype(auto) authAndAccess(Container c, Index i);在这个模板中我们不知道我们操纵的容器的类型是什么那意味着我们同样不知道它使用的索引对象index objects的类型对一个未知类型的对象使用传值通常会造成不必要的拷贝对程序的性能有极大的影响还会造成对象切片行为参见条款41以及给同事落下笑柄。但是就容器索引来说我们遵照标准模板库对于索引的处理是有理由的比如std::stringstd::vector和std::deque的operator[]所以我们坚持传值调用。
然而我们还需要更新一下模板的实现让它能听从条款25的告诫应用std::forward实现通用引用
templatetypename Container, typename Index //最终的C14版本
decltype(auto)
authAndAccess(Container c, Index i)
{authenticateUser();return std::forwardContainer(c)[i];
}这样就能对我们的期望交上一份满意的答卷但是这要求编译器支持C14。如果你没有这样的编译器你还需要使用C11版本的模板它看起来和C14版本的极为相似除了你不得不指定函数返回类型之外
templatetypename Container, typename Index //最终的C11版本
auto
authAndAccess(Container c, Index i)
-decltype(std::forwardContainer(c)[i])
{authenticateUser();return std::forwardContainer(c)[i];
}另一个问题是就像在本条款的开始唠叨的那样decltype通常会产生你期望的结果但并不总是这样。在极少数情况下它产生的结果可能让你很惊讶。老实说如果你不是一个大型库的实现者你不太可能会遇到这些异常情况。
为了完全理解decltype的行为你需要熟悉一些特殊情况。它们大多数都太过晦涩以至于几乎没有书进行有过权威的讨论这本书也不例外但是其中的一个会让我们更加理解decltype的使用。
将decltype应用于变量名会产生该变量名的声明类型。虽然变量名都是左值表达式但这不会影响decltype的行为。译者注这里是说对于单纯的变量名decltype只会返回变量的声明类型然而对于比单纯的变量名更复杂的左值表达式decltype可以确保报告的类型始终是左值引用。也就是说如果一个不是单纯变量名的左值表达式的类型是T那么decltype会把这个表达式的类型报告为T。这几乎没有什么太大影响因为大多数左值表达式的类型天生具备一个左值引用修饰符。例如返回左值的函数总是返回左值引用。
这个行为暗含的意义值得我们注意在
int x 0;中x是一个变量的名字所以decltype(x)是int。但是如果用一个小括号包覆这个名字比如这样(x) 就会产生一个比名字更复杂的表达式。对于名字来说x是一个左值C11定义了表达式(x)也是一个左值。因此decltype((x))是int。用小括号覆盖一个名字可以改变decltype对于名字产生的结果。
在C11中这稍微有点奇怪但是由于C14允许了decltype(auto)的使用这意味着你在函数返回语句中细微的改变就可以影响类型的推导
decltype(auto) f1()
{int x 0;…return x; //decltype(x是int所以f1返回int
}decltype(auto) f2()
{int x 0;return (x); //decltype((x))是int所以f2返回int
}注意不仅f2的返回类型不同于f1而且它还引用了一个局部变量这样的代码将会把你送上未定义行为的特快列车一辆你绝对不想上第二次的车。
当使用decltype(auto)的时候一定要加倍的小心在表达式中看起来无足轻重的细节将会影响到decltype(auto)的推导结果。为了确认类型推导是否产出了你想要的结果请参见条款4描述的那些技术。
同时你也不应该忽略decltype这块大蛋糕。没错decltype单独使用或者与auto一起用可能会偶尔产生一些令人惊讶的结果但那毕竟是少数情况。通常decltype都会产生你想要的结果尤其是当你对一个变量使用decltype时因为在这种情况下decltype只是做一件本分之事它产出变量的声明类型。
要点
decltype总是不加修改的产生变量或者表达式的类型。对于T类型的不是单纯的变量名的左值表达式decltype总是产出T的引用即T。C14支持decltype(auto)就像auto一样推导出类型但是它使用decltype的规则进行推导。
条款4掌握查看型别推导结果的方法
IDE编辑器
在IDE中的代码编辑器通常可以显示程序代码中变量函数参数的类型你只需要简单的把鼠标移到它们的上面举个例子有这样的代码中
const int theAnswer 42;auto x theAnswer;
auto y theAnswer;IDE编辑器可以直接显示x推导的结果为inty推导的结果为const int*。
为此你的代码必须或多或少的处于可编译状态因为IDE之所以能提供这些信息是因为一个C编译器或者至少是前端中的一个部分运行于IDE中。如果这个编译器对你的代码不能做出有意义的分析或者推导它就不会显示推导的结果。
对于像int这样简单的推导IDE产生的信息通常令人很满意。正如我们将看到的如果更复杂的类型出现时IDE提供的信息就几乎没有什么用了。
编译器诊断
另一个获得推导结果的方法是使用编译器出错时提供的错误消息。这些错误消息无形的提到了造成我们编译错误的类型是什么。
举个例子假如我们想看到之前那段代码中x和y的类型我们可以首先声明一个类模板但不定义。就像这样
templatetypename T //只对TD进行声明
class TD; //TD Type Displayer如果尝试实例化这个类模板就会引出一个错误消息因为这里没有用来实例化的类模板定义。为了查看x和y的类型只需要使用它们的类型去实例化TD
TDdecltype(x) xType; //引出包含x和y
TDdecltype(y) yType; //的类型的错误消息使用“变量名字Type”的结构来命名变量因为这样它们产生的错误消息可以有助于我们查找。对于上面的代码我的编译器产生了这样的错误信息我取一部分贴到下面
error: aggregate TDint xType has incomplete type and cannot be defined
error: aggregate TDconst int * yType has incomplete type andcannot be defined另一个编译器也产生了一样的错误只是格式稍微改变了一下
error: xType uses undefined class TDint
error: yType uses undefined class TDconst int *除了格式不同外几乎所有我测试过的编译器都产生了这样有用的错误消息。
运行时输出
使用printf的方法使类型信息只有在运行时才会显示出来尽管我不是非常建议你使用printf但是它提供了一种格式化输出的方法。现在唯一的问题是只需对于你关心的变量使用一种优雅的文本表示。“这有什么难的“你这样想”这正是typeid和std::type_info::name的价值所在”。为了实现我们想要查看x和y的类型的需求你可能会这样写
std::cout typeid(x).name() \n; //显示x和y的类型
std::cout typeid(y).name() \n;这种方法对一个对象如x或y调用typeid产生一个std::type_info的对象然后std::type_info里面的成员函数name()来产生一个C风格的字符串即一个const char*表示变量的名字。
调用std::type_info::name不保证返回任何有意义的东西但是库的实现者尝试尽量使它们返回的结果有用。实现者们对于“有用”有不同的理解。举个例子GNU和Clang环境下x的类型会显示为”i“y会显示为”PKi“这样的输出你必须要问问编译器实现者们才能知道他们的意义”i“表示”int“”PK“表示”pointer to konst const“指向常量的指针。这些编译器都提供一个工具cfilt解释这些“混乱的”类型Microsoft的编译器输出得更直白一些对于x输出”int“对于y输出”int const *“
因为对于x和y来说这样的结果是正确的你可能认为问题已经接近了别急考虑一个更复杂的例子
templatetypename T //要调用的模板函数
void f(const T param);std::vectorWidget createVec(); //工厂函数const auto vw createVec(); //使用工厂函数返回值初始化vwif (!vw.empty()){f(vw[0]); //调用f…
}在这段代码中包含了一个用户定义的类型Widget一个STL容器std::vector和一个auto变量vw这个更现实的情况是你可能在会遇到的并且想获得他们类型推导的结果比如模板类型形参T比如函数f形参param。
从这里中我们不难看出typeid的问题所在。我们在f中添加一些代码来显示类型
templatetypename T
void f(const T param)
{using std::cout;cout T typeid(T).name() \n; //显示Tcout param typeid(param).name() \n; //显示… //param
} //的类型GNU和Clang执行这段代码将会输出这样的结果
T PK6Widget
param PK6Widget我们早就知道在这些编译器中PK表示“pointer to const”所以只有数字6对我们来说是神奇的。其实数字是类名称Widget的字符串长度所以这些编译器告诉我们T和param都是const Widget*。
Microsoft的编译器也同意上述言论
T class Widget const *
param class Widget const *这三个独立的编译器产生了相同的信息并表示信息非常准确当然看起来不是那么准确。在模板f中param的声明类型是const T。难道你们不觉得T和param类型相同很奇怪吗比如T是intparam的类型应该是const int而不是相同类型才对吧。
遗憾的是事实就是这样std::type_info::name的结果并不总是可信的就像上面一样三个编译器对param的报告都是错误的。因为它们本质上可以不正确因为std::type_info::name规范批准像传值形参一样来对待这些类型。正如条款1提到的如果传递的是一个引用那么引用部分reference-ness将被忽略如果忽略后还具有const或者volatile那么常量性constness或者易变性volatileness也会被忽略。那就是为什么param的类型const Widget * const 会输出为const Widget *首先引用被忽略然后这个指针自身的常量性constness被忽略剩下的就是指针指向一个常量对象。
同样遗憾的是IDE编辑器显示的类型信息也不总是可靠的或者说不总是有用的。还是一样的例子一个IDE编辑器可能会把T的类型显示为我没有胡编乱造
const
std::_Simple_typesstd::_Wrap_allocstd::_Vec_base_typesWidget,
std::allocatorWidget::_Alloc::value_type::value_type *同样把param的类型显示为
const std::_Simple_types...::value_type *const 这个比起T来说要简单一些但是如果你不知道“...”表示编译器忽略T的部分类型那么可能你还是会产生困惑。如果你运气好点你的IDE可能表现得比这个要好一些。
比起运气如果你更倾向于依赖库那么你乐意被告知std::type_info::name和IDE不怎么好Boost的TypeIndex库通常写作Boost.TypeIndex是更好的选择。这个库不是标准C的一部分也不是IDE或者TD这样的模板。Boost库可在boost.com获得是跨平台开源有良好的开源协议的库这意味着使用Boost和STL一样具有高度可移植性。
这里是如何使用Boost.TypeIndex得到f的类型的代码
#include boost/type_index.hpptemplatetypename T
void f(const T param)
{using std::cout;using boost::typeindex::type_id_with_cvr;//显示Tcout T type_id_with_cvrT().pretty_name() \n;//显示param类型cout param type_id_with_cvrdecltype(param)().pretty_name() \n;
}boost::typeindex::type_id_with_cvr获取一个类型实参我们想获得相应信息的那个类型它不消除实参的constvolatile和引用修饰符因此模板名中有“with_cvr”。结果是一个boost::typeindex::type_index对象它的pretty_name成员函数输出一个std::string包含我们能看懂的类型表示。 基于这个f的实现版本再次考虑那个使用typeid时获取param类型信息出错的调用
std::vetorWidget createVec(); //工厂函数
const auto vw createVec(); //使用工厂函数返回值初始化vw
if (!vw.empty()){f(vw[0]); //调用f…
}在GNU和Clang的编译器环境下使用Boost.TypeIndex版本的f最后会产生下面的准确的输出
T Widget const *
param Widget const * const在Microsoft的编译器环境下结果也是极其相似
T class Widget const *
param class Widget const * const 这样近乎一致的结果是很不错的但是请记住IDE编译器错误诊断或者像Boost.TypeIndex这样的库只是用来帮助你理解编译器推导的类型是什么。它们是有用的但是作为本章结束语我想说它们根本不能替代你对Item1-3提到的类型推导的理解。
要点
类型推断可以从IDE看出从编译器报错看出从Boost TypeIndex库的使用看出这些工具可能既不准确也无帮助所以理解C类型推导规则才是最重要的
参考Effective Modern C中文版和这里。