拷贝省略和虚拟克隆
copy elision and virtual cloning
以下场景如何避免不必要的复制? Class A 包含指向大对象的基类型指针。
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 对象并用它来构造 class A:
A a{BigDerivedClass()};
或
BigDerivedClass f(){
BigDerivedClass b;
/*constructing object*/
return b;
}
A a{f()};
这里不需要复制创建的对象然后删除它。可以直接在堆中创建此对象并将其地址存储在 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 是一个基 class,将 BigDerivedClass 移动到 BigBaseClass 只会移动 BigBaseClass部分。但是使用智能指针也是个好主意,除非您确定其余代码没有异常。
编译器将不会通过clone()
省略副本的构造:只有在非常特殊的情况下才允许省略副本。在允许编译器进行复制省略的所有情况下,所涉及对象的生命周期完全由编译器控制。四种情况是(详见12.8[class.copy]第8段):
- 按值返回本地名称。
- 抛出一个本地对象。
- 正在复制未绑定到引用的临时对象。
- 按价值捕捉时。
即使在这些情况下复制省略也适用的细节有些重要。无论如何,return new T(*this);
都不适合这些情况。
典型的大对象不会将它们的数据作为对象的一部分保存。相反,它们通常包含一些可以移动的数据结构。如果你想在使用 A{f()}
时保持简单而不想复制 f()
的结果,你可以使用移动构造函数调用 virtual
函数来传输内容而不是复制它:
#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()};
}
移动构造是否有助于复制大对象确实取决于对象的内部实现方式。如果它们的对象本质上只包含几个指向实际大数据的指针,则移动构造应避免任何相关成本,因为与设置实际数据相比,传输指针可以忽略不计。如果数据实际上保存在对象中,则传输不会真正有帮助(尽管出于各种原因,无论如何这样做通常不是一个好主意)。
以下场景如何避免不必要的复制? Class A 包含指向大对象的基类型指针。
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 对象并用它来构造 class A:
A a{BigDerivedClass()};
或
BigDerivedClass f(){
BigDerivedClass b;
/*constructing object*/
return b;
}
A a{f()};
这里不需要复制创建的对象然后删除它。可以直接在堆中创建此对象并将其地址存储在 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 是一个基 class,将 BigDerivedClass 移动到 BigBaseClass 只会移动 BigBaseClass部分。但是使用智能指针也是个好主意,除非您确定其余代码没有异常。
编译器将不会通过clone()
省略副本的构造:只有在非常特殊的情况下才允许省略副本。在允许编译器进行复制省略的所有情况下,所涉及对象的生命周期完全由编译器控制。四种情况是(详见12.8[class.copy]第8段):
- 按值返回本地名称。
- 抛出一个本地对象。
- 正在复制未绑定到引用的临时对象。
- 按价值捕捉时。
即使在这些情况下复制省略也适用的细节有些重要。无论如何,return new T(*this);
都不适合这些情况。
典型的大对象不会将它们的数据作为对象的一部分保存。相反,它们通常包含一些可以移动的数据结构。如果你想在使用 A{f()}
时保持简单而不想复制 f()
的结果,你可以使用移动构造函数调用 virtual
函数来传输内容而不是复制它:
#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()};
}
移动构造是否有助于复制大对象确实取决于对象的内部实现方式。如果它们的对象本质上只包含几个指向实际大数据的指针,则移动构造应避免任何相关成本,因为与设置实际数据相比,传输指针可以忽略不计。如果数据实际上保存在对象中,则传输不会真正有帮助(尽管出于各种原因,无论如何这样做通常不是一个好主意)。