所在的位置: C++ >> C++前景 >> C的decltype和d

C的decltype和d

1decltype()说明符

1.1decltype()的定义

decltype()说明符是C++11引入的一个语言特性,用于检查一个实体(entity)的声明类型,或者一个表达式(expression)的类型和值类别。decltype是“declaretype”的缩写,可以理解是“声明类型”。但是decltype()说明符的作用不是简单的声明,它还会自动推导类型。decltype()说明符的语法定义为:

decltype(entity)decltype(expression)

对于decltype()的返回值有以下4个说明:

(1)如果entity是一个没有带括号的标识符表达式(identifierexpression,id-expression)或者是没带括号的类(或struct)成员访问表达式,则decltype()返回这个entity所命名的实体的类型;

(2)如果expression是一个关于T的表达式,且其值是将亡值类型的右值,则decltype()的结果是T类型;

(3)如果expression是一个关于T的表达式,且其值是左值类型,则decltype()的结果是T类型;

(4)如果expression是一个关于T的表达式,且其值是纯右值类型,则decltype()的结果是T类型;

简单地理解,decltype()对于标识符表达式,返回的结果就是标识符的定义类型,对于其他表达式类型,则进行自动推导。这里对第1条的标识符表达式做个解释。在C/C++中,一个命名函数、变量、枚举项的标识符同时也是一个表达式,当标识符作为一个表达式时,就被称为标识符表达式,该表达式的结果是这个标识符所命名的实体。举个例子,charsdata[32]定义了一个数组,其中sdata就是标识符,当它单独出现时就是标识符表达式,而sdata[0]、sdata[3]+5之类的表达式就不是标识符表达式。那么,加个括号就这么神奇吗?是的,就是这么神奇,因为括号改变了求值顺序,decltype()在推导类型之前,必须先对括号内的表达式的值进行评估,然后对评估的结果进行推导。所以,加了括号之后,原来的标识符表达式就变成了普通的表达式,推导的结果就从实体的类型变成了表达式的类型。看这个例子:

structA{doublex;};constA*a;static_assert(std::is_same_vdecltype(A::x),double);//带限定的标识符表达式,A::x实体的类型是doublestatic_assert(std::is_same_vdecltype((A::x)),double);//加括号了,符合第3条static_assert(std::is_same_vdecltype(a-x),double);//x是double类型static_assert(std::is_same_vdecltype((a-x)),constdouble);//(a-x)是左值表达式

不加括号时,a-x是成员访问表达式,表示要推导的是a中名字为x的这个实体的类型,当然就是double类型了。加了括号就变成了左值表达式。因为a是个const指针,所以a-x是个constdouble,根据第3条的说明,decltype((a-x))的结果就是constdouble类型。另外需要说明一点,decltype()的类型推导是在编译期间完成的,所以可以用静态断言static_assert对结果进行断言。

再看下面这个例子:

inti=0;decltype(i)j=4;//正确,decltype(i)得到的是int类型decltype((i))k=4;//错误,decltype((i))得到的是int类型,只能绑定到一个int变量上decltype((i))m=i;//正确,相当于intm=i

表达式(i)的结果是一个左值类型,所以得到一个int类型,只能绑定到一个左值上,4是右值,所以不正确。

1.2decltype()的使用

decltype()可用于普通函数或成员函数的类型推导:

constintgetRef(constint*p){return*p;}static_assert(std::is_same_vdecltype(getRef),constint(constint*));//getRef是标识符表达式,符合第1条structB{doubleGetXValue(){return5.4;}};double(B::*p)()=B::GetXValue;static_assert(std::is_same_vdecltype(p),double(B::*)());static_assert(std::is_same_vdecltype(B::GetXValue),double(B::*)());

1.2.2左值和右值

根据1.1节的说明2-4,对于类型为T的表达式的类型推导,主要看表达式是左值还是右值,以及何种右值。在下面这个例子中,i++是一个典型的纯右值,但是++i是一个左值。很多人简单地认为凡是表达式都是右值,其实是不正确的。

static_assert(std::is_same_vdecltype(i++),int);//i++是纯右值static_assert(std::is_same_vdecltype(++i),int);//++i是左值intd1,d2;static_assert(std::is_same_vdecltype(d1+d2),int);//d1+d2是右值static_assert(std::is_same_vdecltype(d1=d2),int);//d1=d2是左值字符串字面量是左值,很多人认为它是右值,但是它是const类型的左值:

static_assert(std::is_same_vdecltype("hello"),constchar()[6]);//"hello"类型是constchar[6]

数字字面量是纯右值:

static_assert(std::is_same_vdecltype(42),int);右值引用具名后可当左值,但是从类型上理解,它依然是右值引用:

intrri=std::move(i);rri=10;static_assert(std::is_same_vdecltype(rri),int);//符合第1条说明

将亡值得到相关的右值引用类型:

static_assert(std::is_same_vdecltype(std::move(i)),int);函数调用的类型由函数返回值类型决定:

constintgetRef(constint*p);constintFunc();static_assert(std::is_same_vdecltype(getRef(nullptr)),constint);static_assert(std::is_same_vdecltype(Func()),constint);

1.3C++17的变化

C++17因为引入了结构化绑定等语法元素,所以对decltype()引入了3条新的说明:

(5)如果entity是没有括号的结构化绑定变量名(标识符表达式),则decltype()的结果与std::tuple_elementN()的结果和cv限定符(cv-qualified)有关,与引用限定符(ref-qualifier)无关;

(6)如果expression是一个返回类类型的纯右值,或是最右边是个函数调用的逗号表达式,则decltype()推导类型时不会为这个纯右值生成临时对象;

(7)如果expression是一个纯右值(prvalue)而不是一个(可能带括号的)立即调用(immediateinvocation),则临时对象不会从这个纯右值具体化,因为这样的纯右值不是一个需要结果的对象;

对第5条解释一下,对于结构化绑定变量的类型,由支持结构化绑定对象的std::tuple_elementN()返回的类型决定,与结构化绑定使用的引用限定符(ref-qualifier)无关。以下面的代码为例:

std::tupleint,inttp(42,i);auto[x,y]=tp;static_assert(std::is_same_vdecltype(x),int);\\intstatic_assert(std::is_same_vdecltype(y),int);\\intauto[rx,ry]=tp;//无论是使用还是,结果都一样static_assert(std::is_same_vdecltype(rx),int);static_assert(std::is_same_vdecltype(ry),int);

虽然与引用限定符无关,但是结构化绑定的cv限定符(cv-qualified)却会对结果产生影响。但是如果cv限定符与std::tuple_elementN()得到的结果冲突,则cv的限定失效。以const限定为例,看一下下面的例子:

intk=42;std::tupleint,inttp(42,k);constauto[cx,cy]=tp;static_assert(std::is_same_vdecltype(cx),constint);static_assert(std::is_same_vdecltype(cy),int);//以std::tuple_elementN()返回结果为准

在这个例子中,cx与tp的第1个元素的值是单向绑定,cx的值是42,但是与tp的第一个元素就没有任何关系了,所以此时const限定符对cx是生效的,cx的类型就会被推导为constint类型。但是tp的第2个元素是对k的引用,结构化绑定变量cy也是对k的引用,此时const限定符就会产生冲突,因为依然可以通过对k的赋值同时影响tp和cy,是的const的限制没有意义,所以cy的类型仍然被推导为int。

对第6条和第7条的解释就是为了效率。类型推导过程中如果产生临时对象,则会产生临时对象的创建和销毁开销,所以第6条针对这种情况不生成临时对象,自然就提高了程序的效率。对于一个完整定义的类型T,T的纯右值有可能会被转移到一个临时创建的T类型的将亡值上,即使是支持移动语义,这个将亡值的初始化也需要开销,如果此时的纯右值是一个没有结果的对象,那么这个将亡值也就不需要初始化(从纯右值上移动数据)。所以第7条的目的也是为了效率,这些都是编译器层面的动作优化,代码层面很难观察到效果。立即函数(immediatefunction)是C++20增加的内容,我猜测可能因为立即函数调用是在编译期就已经求值并得到结果,所以无法再提供类似的优化,所以排除一下。

由于不需要创建临时对象,所以类型T不需要在此时提供完整的定义,其析构函数甚至可以是纯虚函数。但是这个规则不适用于嵌套的子表达式,比如decltype(f(g())),f()返回类型可以不提供完整定义,但是g()返回的类型必须有完整定义。

1.4C++20的变化

C++20补充了一个新说明:

(8)如果expression是一个不带括号的标识符表达式,对应一个不确定类型的模板参数的名字,此时decltype()也能得到模板参数的类型,如果模板参数只是一个占位符声明,则decltype()会执行必要的推导(替换)。另外,即使对象是个常量对象(const),decltype()对这个模板参数推导的结果也是实体对应的非常量(non-const)类型。

对于这条说明,还是用例子比较好解释。下面的代码中,根据替换原则,T应该是constint类型,但是decltype(t)的结果却是int类型:

templatetypenameT,typenameUautoMyFunc(Tt,Uu)-decltype(t){returnstd::forwardT(t)+std::forwardU(u);};constintt=4;constintu=2;autor=MyFunc(t,u);//尽管t是constint,但是r是int类型

2decltype()与auto

auto和decltype关键字都可以自动推导出变量的类型,但它们的用法是有区别的。auto使用时需要静态初始化语句,编译器需要根据变量声明的初始化表达式确定实际类型,而decltype()只需要表达式的结果不是void类型,就可以从任何表达式推导其对应的类型。并且decltype()并不会真的对表达式求值,其类型推导是在编译期完成的,所以decltype()更适合在泛型编程中使用。

在C++14之后,auto甚至可以在一些模板参数中充当占位符的作用,用decltype(auto)来进行类型推导,比如这个函数模板:

templatetypenameT,typenameUdecltype(auto)myFunc(Tt,Uu){returnforwardT(t)+forwardU(u);};

如果是传统的用法,则需要显式用-指定函数的返回值类型:

templatetypenameT,typenameUautomyFunc(Tt,Uu)-decltype(forwardT(t)+forwardU(u)){returnforwardT(t)+forwardU(u);};

3std::declval()

std::declval()的作用是返回任意类型T的右值引用类型T,其定义如下:

templateclassTtypenamestd::add_rvalue_referenceT::typedeclval()noexcept;

可见其实现是借用了std::add_rvalue_referenceT,这是一个typetraits,在《C++的typetraits1-3》相关主题中介绍过std::add_rvalue_referenceT,这里不再赘述。需要注意,和decltype()不一样,std::declval()是标准库中的内容,使用时需要带"std::"。

即使T没有默认的构造函数,或者不可以创建T的实例对象(比如抽象基类),std::declval()也依然可以得到T。std::declval()只有声明,没有实现,所以它是不可调用的,也就是说这样用是非法的:

std::declvalint()kk=std::move(i);//希望结果是intkk=std::move(i);

std::declval一般和decltype配合使用,在泛型编程中用于类型推导。

structDefault{intfoo()const{return1;}};structNonDefault{NonDefault()=delete;intfoo()const{return1;}};decltype(Default().foo())n1=1;//可以,Default有默认构造函数n1的类型是intdecltype(NonDefault().foo())n2=n1;//错误,NonDefault默认构造函数用delete修饰,不能构造NonDefault对象decltype(std::declvalNonDefault().foo())n2=n1;//std::declval可以不构造对象的情况下得到NonDefault类型NonDefault不生成默认构造函数,因此不可以通过NonDefault()构造一个临时对象,然后调用foo()函数。但是std::declvalNonDefault()的作用就是得到一个NonDefault类型,可以借助这个NonDefault的右值引用类型调用foo()函数。最终的效果是在没有构造任何NonDefault对象实例的情况下就调用NonDefault的成员函数。std::declval()还可以获取某个成员函数返回值的类型信息,比如配合sizeof()获取大小:

structA{};structNonDefault{NonDefault()=delete;intfoo()const{return1;}AGetA(){returnm_a;}Am_a;};std::coutsizeof(std::declvalNonDefault().GetA())std::endl;//得到A的大小

当然也可以配合其他的typetraits获取类型的其他信息,都不需要构造NonDefault对象的实例。

往期内容:

C++的内存分配和释放

C++的“结构化绑定(StructuredBinding)”

C++的“静态初始化顺序难题”

C++的Magicstatics与局部静态变量

C++的复制消除(CopyElision)和有保证的复制消除(GuaranteedCopyElision)

C++11的codecvt与编码转换

C++的pair和tuple

C++的僵尸标识符(Zombieidentifiers)(截止C++23)C++的存储类型与新的thread_local

C++的多线程内存模型和memory_order

C++的字面量(literal)

C++和C的版本关系

怎样理解“模板成员函数不能偏特化”

再读《架构整洁之道》

C++20,说说Module那点事儿

目不识丁

您的支持是我们前进的动力




转载请注明:http://www.aierlanlan.com/cyrz/502.html