保证副本省略不省略副本

这篇文章也可以在 西蒙·布兰德的博客

null

C++ 17合并在一个名为 通过简化的值类别保证拷贝省略 . 这些变更规定,在以前允许复制或移动的某些情况下,不得进行复制或移动,例如:

 
struct non_moveable { 
    non_moveable() = default; 
    non_moveable(non_moveable&&) = delete; 
}; 
non_moveable make() { return {}; } 
non_moveable x = make(); //compiles in C++17, error in C++11/14 

您可以在编译器版本中看到这种行为 Visual Studio 2017年 15.6、Clang 4、GCC 7及以上。

不管报纸的名字和你在网上看到的内容, 新规则不保证删改 . 相反,定义新的值类别规则时,首先不存在副本。理解这个细微差别,可以更深入地理解当前C++对象模型,所以我将解释C++ 17规则,做了哪些改变,以及它们如何解决现实世界问题。

价值类别

为了理解before和after,我们首先需要了解什么是值类别(我将在下一节解释复制省略)。继续C++的误称主题,价值范畴不是价值范畴;它们是表情的特征。C++中的每个表达式都有三个值类别之一: 左值 , pr值 (纯右值),或 X值 (过期值)。然后有两个超级类别,如下图所示。

diagram expression the taxonomy described above

对于这些是什么的解释,我们可以看看标准( C++ 17 [基本语言。 ):

  1. glvalue[(广义左值)]是一个表达式,其求值确定对象、位字段或函数的同一性。
  2. prvalue是一个表达式,它的求值初始化对象或位字段,或计算运算符的操作数的值,如它出现的上下文所指定的。
  3. xvalue是一个glvalue,它表示一个对象或位字段,该对象或位字段的资源可以重用(通常是因为它的生命周期接近尾声)。
  4. 左值是不是xvalue的glvalue。
  5. rvalue是prvalue或xvalue。

一些例子:

 
std::string s;  
s //lvalue: identity of an object  
s + " cake" //prvalue: could perform initialization/compute a value  

std::string f();  
std::string& g();  
std::string&& h();  

f() //prvalue: could perform initialization/compute a value  
g() //lvalue: identity of an object  
h() //xvalue: denotes an object whose resources can be reused  

struct foo {  
    std::string s;  
};  

foo{}.s //xvalue: denotes an object whose resources can be reused 

C++ 11

表达式的属性是什么 std::string{"a pony"} ?

这是一个prvalue。它的类型是 std::string . 它有价值 "a pony" . 它命名了一个临时的。

最后一个是我想讨论的关键点,这是C++ 11规则和C++ 17之间的真正区别。C++ 11中, std::string{"a pony"} 确实是临时的。从 C++ 11 [类]临时/ 1 :

类类型的临时变量是在各种上下文中创建的:绑定对prvalue的引用、返回prvalue、创建prvalue的转换、引发异常、输入处理程序,以及在某些初始化中。[…]

让我们看看它是如何与代码交互的:

 
struct copyable { 
    copyable() = default; 
    copyable(copyable const&) { /*...*/ } 
}; 
copyable make() { return {}; } 
copyable x = make(); 

make() 结果是暂时的。此临时文件将被移入 x . 自 copyable 没有移动构造函数,这将调用复制构造函数。但是,这个副本是不必要的,因为对象是在 make 永远不会被用于其他任何事情。该标准允许通过在调用站点而不是在中构造返回值来省略此副本 make ( C++11 [class.copy]/31 ). 这叫做 复制省略 .

不幸的是: 即使删除了类型的所有副本,构造函数仍然必须存在 .

这意味着如果我们有:

 
struct non_moveable { 
    non_moveable() = default; 
    non_moveable(non_moveable&&) = delete; 
}; 
non_moveable make() { return {}; } 
auto x = make(); 

然后我们得到一个编译器错误:

(7): error C2280: 'non_moveable::non_moveable(non_moveable &&)': attempting to reference a deleted function 
(3): note: see declaration of 'non_moveable::non_moveable' 
(3): note: 'non_moveable::non_moveable(non_moveable &&)': function was explicitly deleted 

除了按值返回不可移动类型外,这还带来了其他问题:

  1. 禁止对固定类型使用几乎总是自动样式:
     
    auto x = non_moveable{}; //compiler error 
    
  2. 这种语言并不能保证构造函数不会被调用(实际上这并不太令人担心,但是保证比可选的优化更有说服力)。
  3. 如果我们想支持这些用例中的一些,我们需要为它们没有意义的类型编写复制/移动构造函数(然后做什么?扔?中止?链接器错误?)
  4. 你不能通过值将不可移动的类型传递给函数,以防你有一些用例可以帮助你。

解决办法是什么?标准是否应该说“哦,如果你删除了所有副本,你就不需要那些构造函数了”?也许吧,但是所有这些关于构建临时对象的语言实际上都是谎言,建立关于对象模型的直觉变得更加困难。

C++ 17

C++ 17采用了不同的方法。它没有保证在这些情况下,副本将被忽略,而是改变了规则,使副本从一开始就不存在。这是通过重新定义创建临时表来实现的。

如前面的值类别描述中所述,prvalues的存在是为了初始化。C++ 11急切地创建临时,最终使用它们初始化并清理事实之后的副本。在C++ 17中,临时的物化被延迟直到执行初始化。

这是一个更好的名称为这个功能。不保证复制省略。 延迟临时物化 .

临时物化从prvalue创建临时对象,生成xvalue。最常见的情况是将引用绑定到prvalue或对prvalue执行成员访问时。如果引用绑定到PROVE,则将物化临时的生存期扩展到引用的时间(这与C++ 11没有变化,但值得重复)。如果prvalue初始化与prvalue类型相同的类类型,则直接初始化目标对象;不需要临时的。

一些例子:

 
struct foo { 
    int i; 
}; 
 
foo make(); 
auto const& a = make();  //temporary materialized and lifetime-extended 
auto&& b = make(); //ditto 
 
foo{}.i //temporary materialized 
 
auto c = make(); //no temporary materialized 

这涵盖了新规则中最重要的几点。现在来看看为什么这是一个有用的过去的术语,骑自行车和琐事给你的朋友留下深刻印象。

谁在乎呢?

我在开始时说,理解新规则将允许对C++ 17对象模型有更深入的理解。我想在这一点上做进一步的阐述。

关键是在C++ 11中,PR值在某种意义上不是“纯”的。也就是说 std::string{"a pony"} 说出一些临时的名字 std::string 包含内容的对象 "a pony" . 这不是单纯的“小马”的概念。这不是柏拉图的“小马”理想。

然而,在C++ 17中, std::string{"a pony"} 柏拉图式的“小马”理想。在C++的对象模型中,它不是一个真正的对象,它是一些难以捉摸的、无定形的想法,它可以在程序周围传递,而只是在初始化某个结果对象时,或者是临时化的时候被赋予。 C++ 17的PR值是纯PR值 .

如果这一切听起来有点抽象,那没关系,但是内化这个想法会让你更容易对程序的各个方面进行推理。举个简单的例子:

 
struct foo {}; 
auto x = foo{}; 

在C++ 11模型中,PR值 foo{} 创建用于移动构件的临时构件 x ,但这一举动可能被编译器忽略了。

在C++ 17模型中,PR值 foo{} 初始化 x .

一个更复杂的例子:

 
std::string a() { 
    return "a pony"; 
} 
 
std::string b() { 
    return a(); 
} 
 
int main() { 
    auto x = b(); 
} 

在C++ 11模型中, return "a pony"; 初始化的临时返回对象 a() ,其中move构造 b() ,移动构造 x . 所有的动作都可能被编译器忽略。

在C++ 17模型中, return "a pony"; 初始化的结果对象 a() ,它是的结果对象 b() ,即 x .

本质上,初始值设定项不是创建一系列临时变量(理论上,这些临时变量构成一系列返回对象),而是将初始值设定项传送到最终结果对象。

结束

“保证副本省略”规则不保证副本省略;相反,它们净化了pr值,使得副本一开始就不存在。下次你听到或读到关于保证复制省略的文章时,请考虑一下 延迟临时物化 . 即使您不认为术语很重要,这些知识也可以帮助您更容易地解释代码的行为。

自Visual Studio 2017版本15.6以来,MSVC一直支持延迟临时物化/保证副本省略。我们希望你能 下载最新版本 试试看。一如既往,我们欢迎您的反馈。我们可以通过下面的评论或电子邮件联系我们( visualcpp@microsoft.com ). 如果您遇到MSVC的其他问题或对Visual Studio 2017有任何建议,请通过联系我们 帮助>发送反馈>报告问题/提供建议 在产品中,或通过 开发者社区 . 你也可以在Twitter上找到我们( @视觉 )还有Facebook( msftvisualcpp软件 ).

如果您对这篇文章有任何疑问,请使用下面的评论或直接发送给西蒙 西蒙。brand@microsoft.com @鞑靼喇嘛 .

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享