decltype
C++ 规范中,对于 decltype 类型推导规则的定义如下:
decltype(expr)
若expr是一个没有带括号的标记符表达式(如局部变量名、命名空间作用域变量、函数参数等)或者类成员访问表达式(注意,静态数据成员按规则2.2推导),那么的decltype(expr)就是expr所命名的实体的类型(声明型别)。此外,如果expr是一个被重载的函数,则会导致编译错误。
否则:
- 若 expr 的值类别为 将亡值 ,则
decltype产生T&&;- 若 expr 的值类别为 左值 ,则
decltype产生T&;- 若 expr 的值类别为 纯右值 ,则
decltype产生T。
对于纯右值而言,只有类类型可以保留cv限定符,其它类型则会丢失cv限定。
如果 表达式 是返回类类型纯右值的函数调用,或是右操作数为这种函数调用的逗号表达式,那么不会对该纯右值引入临时量。
也就是若表达式是 纯右值 ,则不从该纯右值 物质化 临时对象:这种纯右值无结果对象。
说人话就是类型名+
()等于调用类的默认构造生成匿名临时对象,这个临时对象的类型自然是类类型。临时对象又是纯右值。所以就是你这个表达式是返回类类型纯右值的函数调用。注意如果对象的名字带有括号,则它被当做通常的 左值 表达式,从而
decltype(x)和decltype((x))通常是不同的类型。
- 表达式:表达式由一个或多个运算对象组成,对表达式求值将得到结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
- 标记符表达式:即除去关键字、字面量等编译器所需要使用的标记之外的程序员自定义的标记(token)都是标记符。而单个标记符对应的表达式就是标记符表达式。如
int arr[4];那么arr是一个标记符表达式**,**arr[3],arr[3]+0等都不是标记符表达式。 - 上述“expr所命名的实体的类型”和“expr的类型”是不完全相同的两个概念。在类成员访问表达式(如
E1.E2或E1->E2)中,expr所命名的实体的类型即为E2的“声明类型”,而expr的类型指整个表达式E1.E2求值结果的类型。- 如果E2为静态数据成员,表达式
E1.E2的结果始终是左值。decltype(E1.E2)按规则2.2而不是规则1推导。 - 如果E2是个引用类型(如
T&或T&&),decltype(E1.E2)指E2实体的声明类型(T&或T&&)。而整个表达式E1.E2结果则为T类型的左值,即代表E2引用所指的对象或函数。 - 若E2为非引用类型:当E1为左值时,E1.E2整个表达式为左值。而如果E1为右值,则整个表达式为右值类型。(E2为静态数据成员例外,见1的说明)。
- 如果E2为静态数据成员,表达式
- 字符串字面值常量是个const的左值(可以取地址),采用规则2.2推导。而非字符串字面值常量则是个右值,采用规则2.3推导。
- 对于类型为T的左值表达式,decltype总是得出T&类型
decltype 有两种表达方法:
- 有括号:
decltype((expr))- 有括号的表达方法,语意是简单而统一的:它站在表达式类别的角度求类型。
- 原本不是表达式的类型,加上括号后立刻变为表达式类型。主要针对是变量名和函数名。
- 有括号传入变量名推导出的类型是这个变量左值引用类型。因为变量名作为表达式是左值。左值按照规则产生左值引用类型。
- 无括号:
decltype(expr)- 无括号和有括号只在针对传入变量名和函数名的时候有区别。
- 无括号传入变量名推导出的类型就是这个变量的类型。
注意。一个表达式的结果不是左值就是右值(包括将亡值)。然而一个变量名称本身也是一个表达式。无括号的情况下,除了一种例外,其它情况下,都与有括号场景一致。这个例外就是对于变量(包括常量)名字的直接求类型。这种情况,会返回变量被定义时的类型。然而给变量再包一层括号,这个变量的名字会变成表达式。
也就是
int a = 10;a; //表达式decltype(a); //a被当做变量。类型类型推导为intdecltype((a));//a被当做表达式。表达式a是左值,所以类型推导为int&额外注意作用于函数名会得到函数类型,不会自动转换成指针。
例子:
struct sr{};int func(){}int& anotherfunc(){}int func_with_arg(int a, int b){
}int main() { sr obj; cout << is_same_v<decltype(func), int(void)> << endl; //这个是变量。是函数本身。所以是函数类型。注意不是函数指针类型。 cout << is_same_v<decltype(&func), int(*)(void)> << endl; //这个是变量。是取函数地址。所以是函数指针类型。 cout << is_same_v<decltype(func()), int> << endl; //这是表达式。值传递的返回值是临时对象,纯右值推导为变量类型 cout << is_same_v<decltype(sr()), sr> << endl; //sr()是表达式。类匿名临时对象,纯右值。纯右值推导为变量类型 cout << is_same_v<decltype(anotherfunc()), int&> << endl; //这是表达式。引用返回的是左值。左值推导为引用类型 cout << is_same_v<decltype(obj), sr> << endl; //这是变量。直接作用于变量是变量类型 cout << is_same_v<decltype(move(obj)), sr&&> << endl;//这是表达式。move返回右值引用,表达式返回将亡值。将亡值是右值引用类型
//----加括号部分------ cout << is_same_v<decltype((func)), int(&)(void)> << endl; //注意,加了括号后变成表达式类型。具名函数也算左值,返回引用类型。所以是函数引用。 cout << is_same_v<decltype((func())), int> << endl; //加了括号没区别 cout << is_same_v<decltype((sr())), sr> << endl; //加了括号没区别 cout << is_same_v<decltype((anotherfunc())), int&> << endl; //加了括号没区别 cout << is_same_v<decltype((obj)), sr&> << endl; //这是表达式因为给变量多了一层括号。这个表达式是左值。因为具名变量本身是左值。左值推导为引用类型 cout << is_same_v<decltype((move(obj))), sr&&> << endl;//加了括号没区别
//-------需要强调的部分------- cout << is_same_v<decltype(func_with_arg), int(int,int)> << endl;//这是推导函数类型 cout << is_same_v<decltype(func_with_arg(int{}, int{})), int> << endl;//函数带了括号是表达式。这是推导函数调用的返回值类型 cout << is_same_v<decltype(func_with_arg(3,4)), int> << endl;//和上面一样 cout << is_same_v<decltype(func_with_arg(int,int)), int> << endl;//不行。func_with_arg(int,int)不是有效表达式 cout << is_same_v<decltype(func_with_arg(declval<int>(),declval<int>())), int> << endl;//使用declval。但是是脱裤子放屁
int s = 100; cout << is_same<decltype(2), int>::value << endl; // 字面值纯右值,推导为T cout << is_same<decltype(s), int>::value << endl; //变量名不加括号,推导为其声明类型。此处就是T cout << is_same<decltype((s)), int&>::value << endl; //变量名加了括号是表达式。是左值。此处推导为T& cout << is_same<decltype(move(s)), int&&>::value << endl;//move过后变成将亡值,推导为T&&}- 注意需要强调的部分。函数加括号就是表达式。就是函数调用。推导函数调用类型就等于推导函数返回值类型。
- **注意推导函数调用的时候,如果有参数必须放一个参数而不是一个类型。**比如倒数第二行,
func_with_arg(int,int)并不是有效的表达式,你见过这么调用的函数?就算decltype不求值,但是必须语句合法。- 所以你既可以放一个参数进去,也可以用
{}初始化临时对象。但是不能()因为会被当做函数声明。 - 也可以像最后一行,用declval。但是在这里是脱裤子放屁
- 所以你既可以放一个参数进去,也可以用
https://stackoverflow.com/questions/19200513/function-pointer-vs-function-reference
https://stackoverflow.com/questions/36517596/decltype-parenthesis-syntax-for-a-lvalue
https://modern-cpp.readthedocs.io/zh_CN/latest/decltype.html
https://blog.csdn.net/u014609638/article/details/106987131
https://www.cnblogs.com/5iedu/p/11222078.html
关于decltype成员函数类型
struct A { void func_const() const{
} void func_nonconst(){
} void test(){ cout << is_same_v< decltype(&A::func_nonconst), void(A::*)(void)> << endl; //这是求函数指针类型 cout << is_same_v< decltype(declval<A>().func_nonconst()), void> << endl; //这是求函数返回值类型 }};- 第一个,
&A::func_nonconst并不调用函数,它获取一个成员函数地址,可以隐式转换为成员函数指针。所以此时类型比较的是成员函数指针类型而非成员函数类型。所以是void(A::*)(void)而不是void(void) - 第二个,
declval目的是求函数返回值类型。所以必须要有函数调用。最后必须是func_nonconst()
关于decltype的不求值语境
decltype推导过程是在编译期完成的,并且不会真正计算表达式的值。
decltype不产生代码,对于表达式的要求只需要一个声明,不需要定义。也就是只需要一个名字。所以你可以这样:
int test(); //压根没定义int main(){ cout << is_same<int, decltype(test())>::value << endl;//true}-
虽然decltype并不求值,但是其中涉及到的临时对象的“创建”和”销毁“都是受到语义限制的。也就是仍然受到创建和销毁临时所需的任何函数(包括构造函数)的可访问性的限制。
- 换句话说,其不会生成临时对象,但是也应遵守所有语义限制,就好像临时对象已创建并随后被销毁一样。这包括可访问性 以及它是否被删除,比如造函数和析构函数。
-
https://stackoverflow.com/questions/25663642/why-can-i-use-private-default-constructor-in-decltype
-
此处有疑问。在标准文档N4835的6.7.7一节中,这样描述:
Even when the creation of the temporary object is unevaluated (7.2), all the semantic restrictions shall be respected as if the temporary object had been created and later destroyed. [Note: This includes accessibility (11.9) and whether it is deleted, for the constructor selected and for the destructor. However, in the special case of the operand of a decltype-specifier (7.6.1.2), no temporary is introduced, so the foregoing does not apply to such a prvalue. — end note]
这句话说的由于在 decltype 说明符操作数的特殊情况下,没有引入临时值,因此上述内容不适用于此类 prvalue。但是我们来测试一次
struct A { A() = delete; int foo();};int main(){ decltype(A().foo()); //我们先忽略掉如果decltype合法后提示的 does not declare anything 错误}为什么这里会提示不合法:use of deleted function ‘A::A()’
这里要区分不求值和是否合法。非静态成员函数必须通过一个对象或指向对象的指针调用。就算我们这里没有引入临时值,此时不求值,但是必须要通过对象调用。没有对象无法调用。不合法。同时,在标准库文档有这样的一句话:
如果使用特殊语法 = delete 取代函数体,那么该函数被定义为弃置的(deleted)。任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,**甚至是在不潜在求值的表达式中使用弃置函数。**但是可以隐式 ODR 使用刚好被弃置的非纯虚成员函数。
所以在不潜在求值的表达式中使用弃置函数并不合法。
如果我们换成private呢?其就受到了前面提到的可见性限制。
不过我们如何理解最后一句话呢?其实文档提到的是这个例子:
struct A { A() = delete;};A testfunc(); //注意这里int main(){ decltype(testfunc()); //我们忽略掉does not declare anything 错误}这里我们有一个返回A的全局函数。全局函数直接调用是合法的。所以完全可以调用。此时函数的返回值是个临时对象,符合prvalue要求。此时表达式合法。这时候,就算A的构造函数是delete或private,都会成功。因为结合起来应该是说,创建临时对象的过程在decltype语境里是无视delete和可见性限制的。但是其调用方式必须合法。也就是说它不关心decltype里面那个函数创建的对象的过程。

关于decltype其他的雷点
查看这篇文章的Problems of automatic type deduction一节
还有O‘Dwyer的这篇文章
对指针解引用,返回的是对象的引用

- 这是想当然的。不然你没办法解引用后赋值,如
*ptr = 200; - 我们在EFFSTL中提到了对迭代器解引用返回的是引用。
- 同时在vptr当中提到了解引用指针的多态性。
逐位拷贝 memcpy ,可平凡复制 TriviallyCopyable 和 可平凡重定位 TriviallyRelocatable
在C++中,我们把传统C风格的数据类型叫做POD(Plain Old Data)对象,即一种古老的纯数据,C的所有对象都是POD。一般来说,POD对象应该满足如下特性:其二进制内容是可以随意复制的,无论在什么地方,只要其二进制内容存在,我们就能准确无误地还原POD对象。正是由于这个原因,对于任何POD对象,我们都可以放心大胆地使用memset(),memcpy(),memcmp()等函数对对象的内存数据进行操作。
下列类型统称为可平凡复制类型:
- 标量类型
- 可平凡复制类类型
- 上述类型的数组
- 这些类型的有 cv 限定版本
什么东西不是POD类型?C++的对象大概率并不是一个POD,我们无法像在C中那样获得该对象直观简洁的内存模型。对于POD对象,我们可以通过对象的基地址和数据成员的偏移地址获得数据成员的地址。但是C++标准并未对非POD对象的内存布局做任何定义,对于不同的编译器,其对象布局是不同的。而在C语言中,对象布局仅仅会受到底层硬件系统差异的影响。针对非POD对象,其序列化会遇到一定的障碍:由于对象的不同部分可能存在于不同的地方,因为无法直接复制,只能通过手工加工序列化操作代码来处理对象数据。
最简单的来说,类的特殊成员函数如拷贝构造,拷贝赋值,移动构造,移动赋值和析构函数会为我们隐式添加很多操作。尤其是含有多态的时候。我们在vptr部分详细讲述了。子类对象给父类对象赋值的时候会产生切割。vptr不会被复制, 会隐式的在拷贝构造或拷贝赋值中对其进行调整。但是,memcpy却会完完整整的把二进制级别信息拷贝进来。所以不会针对如vptr进行调整。这时候如果针对非POD类型进行memcpy操作,再调用函数就会爆炸。
当然了,如果子类给子类memcpy,父类给父类进行memcpy,不会发生问题。因为不涉及对如vptr等东西进行隐式调整。
一般来说,只要是我们在语义上和行为上允许不调用析构函数,也就是析构函数不做什么事情的时候,它就是可平凡复制的,可以使用memcpy。来自
在上面的讲座中提到了TriviallyRelocatable可平凡重定位的。它是一个仍然在草案中的特性。重定位就相当于移动+调用原对象的析构函数。因为移动后,被移动的对象的资源虽然移走了,但是对象本身还在,依旧需要调用析构函数。这样就是需要访问两次对象。一次移动和一次析构。而重定义相当于在移动后立即调用对象的析构。也就是只需要访问一次。
可平凡重定位就相当于只需要移动而不需要调用析构函数。因为析构函数不做什么。
对于一个对象
R,如果“移动它然后立即销毁原始对象”的操作等同于memcpy,我们可以说R是可平凡重定位的。
最经典的例子就是当std::unique_ptr的内容被移动后的时候。因为它下面就是个原始指针。这时候当我们把资源移走后,如果我们使用std::move触发移动构造函数,则当前unique_ptr下面的原始指针一定会被置空。所以这个时候调用析构函数是无副作用的。所以它完全可以被省略掉。这个时候可以把处在这个状态下的unique_ptr当做可平凡重定位的。但是,不应该在std::unique_ptr上面施加如此复杂的判断逻辑。所以我们这个时候可以使用memcpy来直接拷贝整个std::unique_ptr对象(相当于把move+析构函数的这一套操作换为memcpy,也就是有意识的浅拷贝)。当然,我们不能销毁被拷贝的std::unique_ptr。因为一旦销毁了,就会调用析构函数,就会导致资源释放而出现问题。
注入类名 (injected-class-name)
- 注入类名是在类的作用域内该类自身的名字。
- 在类作用域中,当前类的名字被当做它如同是一个公开成员名一样;这被称为注入类名(injected-class-name)。该名字的声明点紧跟类定义的开花括号之后。
- 类模板中,注入类名能用作指代当前模板的模板名,或指代当前实例化的类名。
int X;struct X { void f() { X* p; // OK:X 指代注入类名也就是本类名字。 ::X* q; // 错误:名称查找找到变量名,它隐藏了本类名字。 }};- 与其他成员类似,注入类名可被继承,但是依旧受可见性制约。在私有或受保护继承的场合,可能导致某个间接基类的注入类名在派生类中最后变得不可访问。
struct A {};struct B : private A {};struct C : public B { A* p; // 错误:注入类名 A 不可访问 ::A* q; // OK:不使用注入类名};其他的太繁琐了。
局部类 local class
我不知道这玩意有啥用。它就是一个在函数里面的类
struct base{ virtual void func(){ cout <<"b" << endl; }};
base* test(){ struct innerclass:base{ //局部类 继承自base int val = 200; void func(){ cout <<"inner" << endl; } }; return new innerclass;}
int main(){ base* ptr = test(); //可以 ptr->func(); //可以 //innerclass obj; 不可以,不可见。}- 局部类有很多很多的限制:
- 必须在类内实现所有成员函数,不可以拿到类外(函数不可嵌套定义)
- 局部类只能访问外层作用域定义的类型,静态变量以及枚举成员或全局变量。如果局部类定义在某个函数的内部,那么该函数的普通局部变量不能被该局部类使用。
- 类内不可以含有静态数据成员,但可以含有静态函数成员。(reason:静态数据成员要求在类外进行初始化,在程序运行之前完成,并且用(::) 来指明所从属的类名,显然不能实现)
- 不能从外部初始化局部类对象。
- 所以我们如果不提供方法返回局部类的某个变量,则外部无法访问。
- 局部类的特性是可以被当做final。因为外界无法继承一个隐藏在函数内的class。
代理函数 surrogate call function
这个东西在模板11.1.1当中我们提到过。我们先举个例子
template <typename Fcn1, typename Fcn2>struct Surrogate { Surrogate(Fcn1 *f1, Fcn2 *f2) : f1_(f1), f2_(f2) {} operator Fcn1*(){ cout << "Fcn1*" << endl; //Surrogate<Fcn1, Fcn2>::operator Fcn1() [with Fcn1 = void (*)(int); Fcn2 = void (*)(double)] return f1_; } operator Fcn2*(){ cout << "Fcn2*" << endl; return f2_; } Fcn1 *f1_; Fcn2 *f2_;};
void foo (int i){ cout <<"foo" << i << endl;}
void bar (double i){ cout <<"bar" << i << endl;}
int main (){ Surrogate<void(int), void(double)> callable(foo, bar); //注意当前版本是假设传入函数类型。我们也可以换成传入函数指针类型的 callable(10); // calls foo //static_cast<void (*)(int)>(callable.operator void (*)(int)())(10); callable(10.1); // calls bar //static_cast<void (*)(double)>(callable.operator void (*)(double)())(10.1);
return 0;}我们发现我们首先传入了两个函数。然后类型显式指定为函数类型。然后使用两个函数指针去接受这两个函数。
然后定义了两个用户定义转换函数。一个会转换成第一个函数的函数指针类型,一个会转换成第二个函数指针类型。
那么在调用的时候,为什么可以调用呢?
我们不管注释掉的两行里面的static_cast,我们发现是因为我们在对callable进行一个函数调用,也就是实施了函数调用表达式。所以编译器会去寻找可调用对象,比如函数指针,函数,或者是带有operator()的东西。然后发现虽然callable不是这些东西,但是可以转换成其中之一,也就是函数指针。因为根据重载解析规则,这两个用户定义转换函数也被重载解析考虑在内(来自有点看不懂的标准文档)。同时这个时候因为给定的参数是int,根据重载解析规则,两次调用会选择对应的版本。
标准文档翻译:
如果函数调用语法中的后缀表达式
E计算出的是类型为cv T的类对象,则候选函数集中至少包括T的函数调用运算符。T的函数调用运算符是在T的作用域中搜索名称operator()的结果。此外,对于在
T中声明的形式为
operator conversion-type-id () cv-qualifier-seq opt ref-qualifier opt noexcept-specifier opt attribute-specifier-seq opt;的非显式转换函数,其中可选的
cv-qualifier-seq是与cv相同的cv限定符或更高的cv限定符,而conversion-type-id表示类型“返回R的(P1,...,Pn)函数的指针”,或类型“返回R的(P1,...,Pn)函数的引用”,或类型“指向返回R的(P1,...,Pn)函数的指针的引用”,还将考虑具有唯一名称(如下)call-function的代理函数
R call-function ( conversion-type-id F, P1 a1, ..., Pn an) { return F(a1, ..., an); }同样,对于
T的基类中声明的每个非显式转换函数,如果该函数没有被T中的另一个介于该函数和T之间的声明隐藏,则也将向候选函数集中添加代理调用函数。提交到重载解析的参数列表包括函数调用语法中的实参表达式,在其前面是隐含的对象参数(
E)。【注1】:在将调用与函数调用运算符进行比较时,隐含的对象参数将与函数调用运算符的对象参数进行比较。在将调用与代理调用函数进行比较时,隐含的对象参数将与代理调用函数的第一个参数进行比较。
函数指针类型的版本:
template <typename Fcn1, typename Fcn2>struct Surrogate { Surrogate(Fcn1 f1, Fcn2 f2) : f1_(f1), f2_(f2) {} operator Fcn1(){ cout << "Fcn1*" << endl; return f1_; } operator Fcn2(){ cout << "Fcn2*" << endl; return f2_; } Fcn1 f1_; Fcn2 f2_;};
void foo (int i){ cout <<"foo" << i << endl;}
void bar (double i){ cout <<"bar" << i << endl;}
int main (){ Surrogate<void(*)(int), void(*)(double)> callable(foo, bar); callable(10); // calls foo callable(10.1); // calls bar return 0;}