ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

std::move(base_ref) 既是glvalue,又是prvalue

std::move(base_ref) 既是glvalue,又是prvalue 从 c11 引入xvalue的原因这里https://blog.csdn.net/jimy1/article/details/164305579最后一段“动态类型glvalue 的动态类型可以和静态类型不同多态对它调用虚函数、用 typeid/dynamic_cast 都有运行期意义而 prvalue 的动态类型就是静态类型。std::move(base_ref) 必须保留多态行为所以它得是 glvalue。”问题“std::move(base_ref) 必须保留多态行为所以它得是 glvalue”这里的std::move(base_ref)是T右值引用应该是右值引用类型是一个右值。一、综述因果关系说反了一点容易让人误以为“为了保住多态”是std::move设计成 glvalue 的根本原因——其实它只是一个自然推论真正的根本原因更底层。先看结论对不对std::move(base_ref)的返回类型是T一个引用类型一个表达式如果类型是引用它的值类别就是xvalue而 xvalue 属于 glvalue 的一种。所以“它得是 glvalue”这个结论没错而且是必然的——只要std::move返回的是引用类型值类别就锁定是 xvalue无需额外论证。问题出在为什么必须是 glvalue这一步如果只说“因为要保留多态行为”逻辑链是这样的structBase{virtualvoidfoo();};structDerived:Base{voidfoo()override;};Derived d;Basebase_refd;foo(std::move(base_ref));// 期望 std::move 之后动态类型仍是 Derived假设std::move返回的不是Baseglvalue而是Baseprvalue那这个 prvalue 就必须实际构造出一个新的Base对象——而构造这个新对象的唯一方式是从d切片slicing拷贝出一个Base部分。这样一来动态类型退化成Base虚函数调用变成静态绑定的效果其实调用的还是Base::foo确实丢失了多态。这个例子没错但它只是一个具体后果不是std::move设计成 glvalue 的核心动机。更根本的原因是std::move的语义是不创建任何新对象只是把已有对象重新贴上一个’可以被移动’的标签。这一点跟多态与否完全无关对象同一性identitystd::move要让重载决议选中移动构造/移动赋值且这个移动操作要作用在原对象上而不是它的某个副本。glvalue 天然表示指向一个已存在的对象prvalue尤其是 C17 前表示一个即将被构造出来的临时对象二者在语义上就不是一回事。不要求类型可拷贝/可移动就能标记std::move本身应该是零开销的类型转换本质就是static_castT如果它返回 prvalue就意味着要在这一步立刻发生一次构造拷贝或移动这对std::unique_ptr、std::atomic这类只能移动或干脆不能拷贝的类型都会出问题也和避免多余拷贝的初衷背道而驰——这跟多态毫无关系纯不可多态的类型一样会被坑。多态/切片只是identity 破坏之后暴露出来的一个看得见的症状因为切片直接改变了动态类型比较直观。但即使是非多态类型返回 prvalue 也一样错误只是错误表现不是虚函数调错而是多做了一次构造/根本编译不过。更准确的表述应该是std::move只是把左值重新解释成右值引用不产生新对象、不改变被引用对象的同一性。因为它不创建新对象所以它必须是 glvalue具体说是 xvalue而 glvalue 不改变对象同一性这一点自然就带来了一个好处动态类型也随之保留多态行为不受影响。也就是说“保多态是glvalue 这个设计选择的一个正确且重要的推论但不宜倒过来说成是为了保多态所以设计成 glvalue”——真正驱动这个设计的是零开销、不创建对象、保持同一性这个更一般的目标多态只是这个目标下顺带被保住的众多性质之一其他还包括适用于不可拷贝类型、不多花一次构造开销等。二、std::move(base_ref)是不是生成了一个 T 的右值引用类型 调用例如 string(string) 的构造函数是的但这里有一个关键的时间线需要拆开——std::move本身和它触发的构造函数调用是两件事,不要混为一谈。第一步std::move(base_ref)单独发生了什么std::string s1hello;autoxstd::move(s1);// 只看这一行std::move(s1)等价于static_caststd::string(s1)。这一步没有调用任何构造函数没有创建任何新对象只是把s1这个已存在对象的引用视角从左值改写成了右值引用类型std::string值类别是 xvalue它引用的仍然是s1本体x s1恒成立如果这行代码写完就结束了s1里的内容完全没变因为根本没人移动它——std::move只是贴了个标签说这个左值现在允许被移动但它自己不动手。第二步谁真正调用了string(string)std::string s2std::move(s1);// 现在才发生构造这一整行才是完整故事std::move(s1)求值得到一个类型为string的 xvalue这一步仍然没有构造任何东西编译器做重载决议看到右边是个 xvalue且string有string(string)这个重载于是选中它string(string)这个构造函数被调用在它的函数体里真正偷走s1内部的指针/容量等资源构造出s2所以你说的调用string(string)完全正确但那是第 3 步发生的事构造/移动动作的主体是构造函数而不是std::move。std::move只是第 2 步重载决议能选中移动构造函数的前提条件把左值伪装成右值它自己在整个过程里是零开销、无副作用的纯类型转换。回到多态的例子这个区分很重要structBase{virtualvoidfoo();virtual~Base()default;};structDerived:Base{voidfoo()override;};Derived d;Basebase_refd;base_ref.foo();// ① 虚函数调用动态类型是 Derived多态生效Base b2std::move(base_ref);// ② 用 base_ref 构造一个新 Base 对象① 里根本没有std::move直接对base_ref调虚函数Derived::foo被调用这是最直接体现动态类型跟着走的地方。② 里std::move(base_ref)产生Base还是指向那个Derived对象同一性没变但接下来Base b2 ...调用的是Base(Base)移动构造函数——这个构造函数只认识Base部分它按值构造出一个新的Base对象b2Derived独有的部分照样被切掉只是这次是移动切片 move slicing而不是拷贝切片。也就是说即使用了std::move一旦你走到按值构造/按值传参这一步多态照样保不住——因为丢多态的根源是按值构造这个动作本身无论是拷贝构造还是移动构造而不是std::move这个转换步骤。std::move只保证了它自己这一步不切片、不建新对象、动态类型不变后面接的是虚函数调用①还是按值构造②才决定多态最终保没保住。小结你说的没错std::move(base_ref)的类型是Base可以拿去匹配T(T)这样的重载。但那次构造/移动的动作是构造函数做的std::move自己只负责贴标签不创建对象、不改变被引用对象的同一性——这正是它必须是 glvalue而不是 prvalue的根本原因多态是否保得住取决于标签贴完之后你是拿它去做虚函数调用保住还是拿它去做按值构造保不住无论拷贝还是移动。三、是一个右值还是 glvalue这里正好戳中一个经典的概念误区——把 “rvalue / lvalue” 和 “glvalue / prvalue” 当成了同一条分界线的两端其实它们是两条互相交叉的分类维度xvalue 恰恰就活在这个交叉点上。先纠正一个前提rvalue 和 glvalue 不是对立关系C11 引入的值类别体系是按两种独立属性划分的有没有身份identity这个表达式是否指代一个已经存在、有确定地址/生命周期的实体能不能被移动可被移出资源组合出三种基本类别画成经典的树表达式 / \ glvalue rvalue / \ / \ lvalue xvalue prvalue (二者的交集)用文字对应值类别有身份(glvalue性质)可移动(rvalue性质)lvalue✔✘xvalue✔✔prvalue✘✔关键就在这里xvalue 同时具备有身份和可移动两个属性所以它属于 glvalue因为 glvalue lvalue ∪ xvalue凭有身份这条也属于 rvalue因为 rvalue xvalue ∪ prvalue凭可移动这条这两句话不矛盾因为glvalue/prvalue和lvalue/rvalue根本不是同一把尺子量出来的。xvalue 这个类别正是标准委员会专门为了描述std::move这种场景而引入的——它需要一个东西既能像 prvalue 一样触发移动构造/移动赋值的重载决议又能像 lvalue 一样保留这是同一个已存在对象的身份从而保留动态类型、地址不变等性质。回到std::move(base_ref)它的类型是Base值类别是xvalue。所以说它是右值—— 对因为 xvalue ⊂ rvalue它能绑定到T、能触发移动构造函数重载这也是你上一个问题里说调用string(string)能成立的原因。说它是glvalue—— 也对因为 xvalue ⊂ glvalue它依然指代一个已经存在的具体对象就是base_ref引用的那个Derived对象本体这正是动态类型能保留、std::move(base_ref)取到的地址跟base_ref一样的原因虽然内建operator语法上要求 lvalue取地址得绕一下但这个表达式指代哪个对象这个身份是清楚且不变的。一句话消歧义“它是右值” 回答的是能不能被移动/能不能匹配T重载这个问题“它是 glvalue” 回答的是它有没有指代一个已存在的具体对象、身份/动态类型保不保得住这个问题。这是两个不同维度的问题std::move(base_ref)在这两个维度上的答案分别是能和有——合起来正好就是xvalue不是自相矛盾而是 xvalue 这个类别存在的意义所在。顺带一提次要点不影响上面结论严格按标准的措辞表达式的类型这个术语本身是不含引用的引用在表达式求值时已经被折叠掉了Base更准确地说是decltype(std::move(base_ref))的结果而表达式本身的类型是Base真正携带左右值信息的是独立于类型之外的值类别这个属性。这个区分不影响你这次问题的结论只是提一句避免以后被类型 vs 值类别绕进去。
返回列表