C++ 拷贝省略与虚拟克隆
在以下情况下,如何避免不必要的复制?类A包含指向大对象的基类型指针C++ 拷贝省略与虚拟克隆,c++,copy-elision,C++,Copy Elision,在以下情况下,如何避免不必要的复制?类A包含指向大对象的基类型指针 class A{ BigBaseClass *ptr; A(const BigBaseClass& ob); ~A(){delete ptr;} }; 有时我需要复制对象ob。因此,我实施虚拟克隆: class BigBaseClass{ virtual BigBaseClass* clone() {return new BigBaseClass(*this);} }; class BigDerive
class A{
BigBaseClass *ptr;
A(const BigBaseClass& ob);
~A(){delete ptr;}
};
有时我需要复制对象ob。因此,我实施虚拟克隆:
class BigBaseClass{
virtual BigBaseClass* clone() {return new BigBaseClass(*this);}
};
class BigDerivedClass : BigBaseClass{
virtual BigDerivedClass* clone() {return new BigDerivedClass(*this);}
};
A::A(const BigBaseClass& ob):ptr(ob.clone(){}
但有时我会创建临时BigDerivedClass对象并使用它来构造类A:
A a{BigDerivedClass()};
或
这里不需要复制创建的对象,然后将其删除。可以直接在堆中创建此对象,并将其地址存储在a.ptr中
但在我看来,编译器不太可能聪明到可以在这里实现复制省略,或者是吗?。那么,你有什么建议来避免这种不必要的复制呢
class BigBaseClass
{
public:
virtual ~BigBaseClass() {}
virtual BigBaseClass* clone() const { return new BigBaseClass(*this); }
};
class BigDerivedClass : public BigBaseClass
{
public:
BigDerivedClass* clone() const override { return new BigDerivedClass(*this); }
};
class A
{
BigBaseClass *ptr;
public:
explicit A(BigBaseClass* ob);
~A() { delete ptr; }
};
A::A(BigBaseClass* ob) : ptr(ob)
{
}
int main()
{
A a(new BigDerivedClass);
}
您可能认为移动语义是一个好主意,但在这种情况下,这并不真正起作用,因为BigBaseClass是一个基类,而将BigDeriveClass移动到BigBaseClass只会移动BigBaseClass的部分。但是使用智能指针也是一个好主意,除非您确定代码的其余部分没有异常
您可能认为移动语义是一个好主意,但在这种情况下,这并不真正起作用,因为BigBaseClass是一个基类,而将BigDeriveClass移动到BigBaseClass只会移动BigBaseClass的部分。但是,使用智能指针也是个好主意,除非您确信其余代码没有异常。编译器不会通过克隆省略副本的构造:只有在非常特定的情况下才允许省略副本。在允许编译器进行复制省略的所有情况下,所涉及对象的生命周期完全由编译器控制。这四种情况详见12.8[类别副本]第8段:
按值返回本地名称。
投掷一个本地对象。
正在复制未绑定到引用的临时对象。
当按值捕捉时。
即使在这些情况下,复制省略也适用的细节有些不重要。在任何情况下,返回新的T*this;不适合这些情况
典型的大对象不会将其数据作为对象的一部分保存。相反,它们通常保存一些可以移动的数据结构。如果您希望在使用{f}时保持简单性,而不希望复制f的结果,则可以使用move构造函数调用虚拟函数来传输内容,而不是复制内容:
#include <utility>
class BigBaseClass {
public:
virtual ~BigBaseClass() {}
virtual BigBaseClass* clone() const = 0;
virtual BigBaseClass* transfer() && = 0;
};
class A{
BigBaseClass *ptr;
public:
A(BigBaseClass&& obj): ptr(std::move(obj).transfer()) {}
A(BigBaseClass const& obj): ptr(obj.clone()) {}
~A(){delete ptr;}
};
class BigDerivedClass
: public BigBaseClass {
BigDerivedClass(BigDerivedClass const&); // copy the content
BigDerivedClass(BigDerivedClass&&); // transfer the content
BigDerivedClass* clone() const { return new BigDerivedClass(*this); }
BigDerivedClass* transfer() && { return new BigDerivedClass(std::move(*this)); }
};
BigDerivedClass f() {
return BigDerivedClass();
}
int main()
{
A a{f()};
}
移动构造是否有助于复制大对象取决于对象的内部实现方式。如果它们的对象实际上只包含两个指向实际大数据的指针,那么move构造应该避免任何相关成本,因为与设置实际数据相比,转移指针的成本可以忽略不计。如果数据实际上保存在对象中,则传输不会真正起作用,尽管出于各种原因,这样做通常是个坏主意。编译器不会通过克隆省略副本的构造:只有在非常特定的情况下才允许复制省略。在允许编译器进行复制省略的所有情况下,所涉及对象的生命周期完全由编译器控制。这四种情况详见12.8[类别副本]第8段:
按值返回本地名称。
投掷一个本地对象。
正在复制未绑定到引用的临时对象。
当按值捕捉时。
即使在这些情况下,复制省略也适用的细节有些不重要。在任何情况下,返回新的T*this;不适合这些情况
典型的大对象不会将其数据作为对象的一部分保存。相反,它们通常保存一些可以移动的数据结构。如果您希望在使用{f}时保持简单性,而不希望复制f的结果,则可以使用move构造函数调用虚拟函数来传输内容,而不是复制内容:
#include <utility>
class BigBaseClass {
public:
virtual ~BigBaseClass() {}
virtual BigBaseClass* clone() const = 0;
virtual BigBaseClass* transfer() && = 0;
};
class A{
BigBaseClass *ptr;
public:
A(BigBaseClass&& obj): ptr(std::move(obj).transfer()) {}
A(BigBaseClass const& obj): ptr(obj.clone()) {}
~A(){delete ptr;}
};
class BigDerivedClass
: public BigBaseClass {
BigDerivedClass(BigDerivedClass const&); // copy the content
BigDerivedClass(BigDerivedClass&&); // transfer the content
BigDerivedClass* clone() const { return new BigDerivedClass(*this); }
BigDerivedClass* transfer() && { return new BigDerivedClass(std::move(*this)); }
};
BigDerivedClass f() {
return BigDerivedClass();
}
int main()
{
A a{f()};
}
移动构造是否有助于复制大对象取决于对象的内部实现方式。如果它们的对象实际上只包含两个指向实际大数据的指针,那么move构造应该避免任何相关成本,因为与设置实际数据相比,转移指针的成本可以忽略不计。如果数据实际上保存在对象中,则传输不会真正起到帮助作用,尽管出于各种原因,这样做通常是个坏主意。这甚至可以编译吗?您的克隆应该返回动态分配的对象,并且您应该使用动态分配的对象进行初始化。另外,查看std::unique_ptr以避免使用裸指针。哦,对不起,我忘记了克隆函数中的新功能。修正了。这不是移动语义发明的目的吗?如果你用一个接受BigBaseClass*的构造函数替换构造函数,你可以把它分配给ptr,强迫你在堆上分配它。这就是直接在堆上创建对象的方式。一个aBigDerivedClass甚至可以编译吗?你的克隆人应该会回来
动态分配的对象,您应该使用动态分配的对象进行初始化。另外,查看std::unique_ptr以避免使用裸指针。哦,对不起,我忘记了克隆函数中的新功能。修正了。这不是移动语义发明的目的吗?如果你用一个接受BigBaseClass*的构造函数替换构造函数,你可以把它分配给ptr,强迫你在堆上分配它。这就是直接在堆上创建对象的方式。aBigDerivedClass您是说使用两个不同的构造函数——一个使用const引用复制对象,另一个使用指针移动对象?或者你的意思是我可以用aBigDerivedClass_对象进行复制。克隆?后者。获取在堆栈上创建的对象的地址并将其存储在A中可能不是很安全,除非您确定堆栈分配的对象将比A更长寿。最好让它清楚地显示正在发生的事情。好吧,当BigClass_对象由函数的值返回时,这似乎没有什么帮助,但也许这只是意味着我应该避免按值返回大对象。按值返回的对象不应该放在A中,因为它位于堆栈上,当它超出范围时会自动销毁。如果您想同时处理BigBaseClass和BigDerivedClass的实例,那么唯一的方法就是在堆上处理,这意味着需要将对象更新为存在。但是您可以执行由\u value.clone返回的ABigDerivedClass\u对象;您的意思是使用两个不同的构造函数-一个使用const引用复制对象,另一个使用指针移动对象?或者你的意思是我可以用aBigDerivedClass_对象进行复制。克隆?后者。获取在堆栈上创建的对象的地址并将其存储在A中可能不是很安全,除非您确定堆栈分配的对象将比A更长寿。最好让它清楚地显示正在发生的事情。好吧,当BigClass_对象由函数的值返回时,这似乎没有什么帮助,但也许这只是意味着我应该避免按值返回大对象。按值返回的对象不应该放在A中,因为它位于堆栈上,当它超出范围时会自动销毁。如果您想同时处理BigBaseClass和BigDerivedClass的实例,那么唯一的方法就是在堆上处理,这意味着需要将对象更新为存在。但是您可以执行由\u value.clone返回的ABigDerivedClass\u对象;尽管这样做通常是一个坏主意,但出于各种原因,你能列举一些或提供一些与此主题相关的材料的链接吗?@MikeMB:这里有一些:堆栈空间相对有限,导致共定位对象有问题;移动大型物体是不可取的;占用空间大的类往往很难测试;从数组中获取其大小的大型对象往往会施加任意限制。在某些情况下,直接大小较大的对象可能是可以接受的,但在大多数情况下,我看到最好使用实际表示的小句柄。谢谢您的列表。我完全同意关于数组的观点,但除此之外,我在这里有点矛盾:我倾向于将数据应该放在哪里的决定转移到使用方面:如果数据应该放在堆上,就用智能指针包装它。这通常会使课程更简单,效率略高,但当然会使营地复杂化。不过,我并不真正了解可测试性这一部分——它是如何与类的内存占用相联系的?@MimeMB:re-testability:具有较大占用空间的类通常由一组元素或许多不同的组件组成。在后一种情况下,类可能相当复杂,不太可能是特定的可测试类。如果类的占用空间很小,那么它就没有那么复杂,并且由可以单独测试的部分组成。当然,也有例外,如果简单的类仍然很大,事情就可以组合起来。尽管出于各种原因,这样做通常不是个好主意,但你能列举一些或提供一些与此主题相关的材料的链接吗?@MikeMB:这里有一些:堆栈空间相对有限,导致共定位对象有问题;移动大型物体是不可取的;占用空间大的类往往很难测试;从数组中获取其大小的大型对象往往会施加任意限制。在某些情况下,直接大小较大的对象可能是可以接受的,但在大多数情况下,我看到最好使用实际表示的小句柄。谢谢您的列表。我完全同意关于数组的观点,但除此之外,我在这里有点矛盾:我倾向于将数据应该放在哪里的决定转移到使用方面:如果数据应该放在堆上,就用智能指针包装它。这通常会使类变得更简单、更简单
效率更高,但当然会使营地复杂化。不过,我并不真正了解可测试性这一部分——它是如何与类的内存占用相联系的?@MimeMB:re-testability:具有较大占用空间的类通常由一组元素或许多不同的组件组成。在后一种情况下,类可能相当复杂,不太可能是特定的可测试类。如果类的占用空间很小,那么它就没有那么复杂,并且由可以单独测试的部分组成。当然,也有例外,如果简单的类仍然很大,事情就可以组合起来。不过,这种情况比较少见。