关于智能指针和新的 C++11/14 功能,我想知道具有这些功能的类的最佳实践返回值和函数参数类型是什么:
-
一个工厂函数(在类外部),用于创建对象并将其返回给类的用户。 (例如,打开文档并返回可用于访问内容的对象。
-
从工厂函数接受对象、使用它们但不取得所有权的实用程序函数。 (例如,计算文档中字数的函数。
在 返回后保留对对象的引用的函数(如获取对象副本的 UI 组件,以便它可以根据需要在屏幕上绘制内容。
工厂函数的最佳返回类型是什么?
- 如果它是一个原始指针,用户将不得不正确
delete
它,这是有问题的。 - 如果它返回
unique_ptr<>
则用户无法根据需要共享它。 - 如果是
shared_ptr<>
那么我是否必须到处传递shared_ptr<>
类型? 这就是我现在正在做的事情,当我获得循环引用时,它会导致问题,防止对象被自动销毁。
实用程序函数的最佳参数类型是什么?
- 我想按引用传递将避免不必要地增加智能指针引用计数,但这有什么缺点吗? 想到的主要问题是它阻止我将派生类传递给采用基类类型参数的函数。
- 有没有办法让调用方清楚它不会复制对象? (理想情况下,如果函数体尝试复制对象,代码将不会编译。
- 有没有办法使其独立于正在使用的智能指针的类型? (也许采取原始指针?
- 是否可以有一个
const
参数来明确函数不会修改对象,而不会破坏智能指针兼容性?
保留对对象的引用的函数的最佳参数类型是什么?
- 我猜
shared_ptr<>
是这里唯一的选择,这可能意味着工厂类也必须返回shared_ptr<>
,对吧?
下面是一些编译的代码,希望能说明要点。
#include <iostream>
#include <memory>
struct Document {
std::string content;
};
struct UI {
std::shared_ptr<Document> doc;
// This function is not copying the object, but holding a
// reference to it to make sure it doesn't get destroyed.
void setDocument(std::shared_ptr<Document> newDoc) {
this->doc = newDoc;
}
void redraw() {
// do something with this->doc
}
};
// This function does not need to take a copy of the Document, so it
// should access it as efficiently as possible. At the moment it
// creates a whole new shared_ptr object which I feel is inefficient,
// but passing by reference does not work.
// It should also take a const parameter as it isn't modifying the
// object.
int charCount(std::shared_ptr<Document> doc)
{
// I realise this should be a member function inside Document, but
// this is for illustrative purposes.
return doc->content.length();
}
// This function is the same as charCount() but it does modify the
// object.
void appendText(std::shared_ptr<Document> doc)
{
doc->content.append("hello");
return;
}
// Create a derived type that the code above does not know about.
struct TextDocument: public Document {};
std::shared_ptr<TextDocument> createTextDocument()
{
return std::shared_ptr<TextDocument>(new TextDocument());
}
int main(void)
{
UI display;
// Use the factory function to create an instance. As a user of
// this class I don't want to have to worry about deleting the
// instance, but I don't really care what type it is, as long as
// it doesn't stop me from using it the way I need to.
auto doc = createTextDocument();
// Share the instance with the UI, which takes a copy of it for
// later use.
display.setDocument(doc);
// Use a free function which modifies the object.
appendText(doc);
// Use a free function which doesn't modify the object.
std::cout << "Your document has " << charCount(doc)
<< " characters.n";
return 0;
}
我将以相反的顺序回答,以便从简单的案例开始。
从工厂函数接受对象、使用它们但不取得所有权的实用程序函数。(例如,计算文档中字数的函数。
如果要调用工厂函数,则始终通过工厂函数的定义来获得所创建对象的所有权。 我想你的意思是,其他一些客户端首先从工厂获取一个对象,然后希望将其传递给本身不拥有所有权的实用程序函数。
在这种情况下,实用程序函数根本不应该关心如何管理它所操作的对象的所有权。 它应该简单地接受一个(可能是const
的)引用,或者 - 如果"no object"是一个有效的条件 - 一个非拥有的原始指针。 这将最大限度地减少接口之间的耦合,并使实用程序功能最灵活。
在返回后保留对对象的引用的函数(如获取对象副本的 UI 组件,以便它可以根据需要在屏幕上绘制内容。
这些应按值std::shared_ptr
。 这从函数的签名中可以清楚地看出,它们对参数具有共享所有权。
有时,拥有一个对其参数具有唯一所有权的函数(想到构造函数)也是有意义的。 这些应该按值(或右值引用)进行std::unique_ptr
,这也将使语义从签名中清晰出来。
一个工厂函数(在类外部),用于创建对象并将其返回给类的用户。 (例如,打开文档并返回可用于访问内容的对象。
这是困难的,因为std::unique_ptr
和std::shared_ptr
都有很好的论据。 唯一清楚的是,返回拥有的原始指针是不好的。
返回std::unique_ptr
是轻量级的(与返回原始指针相比没有开销),并且传达了工厂函数的正确语义。 调用该函数的人将获得对所制造对象的独占所有权。 如果需要,客户端可以从std::unique_ptr
构造std::shared_ptr
,但代价是动态内存分配。
另一方面,如果客户端无论如何都需要std::shared_ptr
,则让工厂使用std::make_shared
以避免额外的动态内存分配会更有效。 此外,在某些情况下,您只需要使用std::shared_ptr
,例如,如果托管对象的析构函数是非virtual
的,并且智能指针要转换为基类的智能指针。 但是std::shared_ptr
比std::unique_ptr
有更多的开销,所以如果后者足够,我们宁愿尽可能避免这种情况。
因此,最后,我想出了以下准则:
- 如果需要自定义删除程序,请返回
std::shared_ptr
。 - 否则,如果您认为大多数客户无论如何都需要
std::shared_ptr
,请利用std::make_shared
的优化潜力。 - 否则,返回一个
std::unique_ptr
。
当然,您可以通过提供两个工厂函数来避免此问题,一个返回std::unique_ptr
,另一个返回std::shared_ptr
以便每个客户端可以使用最适合其需求的函数。 如果你经常需要这个,我想你可以用一些聪明的模板元编程来抽象大部分冗余。
工厂函数的最佳返回类型是什么?
unique_ptr
最好。它可以防止意外泄漏,如果用户想要使用不同的所有权方案,则可以从指针释放所有权,或将所有权转移到shared_ptr
(具有用于此目的的构造函数)。
实用程序函数的最佳参数类型是什么?
引用,除非程序流非常复杂,以至于对象可能在函数调用期间被销毁,在这种情况下shared_ptr
或weak_ptr
。(无论哪种情况,它都可以引用基类,并根据需要添加const
限定符。
保留对对象的引用的函数的最佳参数类型是什么?
shared_ptr
或unique_ptr
,如果您希望它对对象的生命周期负责,而不是担心它。原始指针或引用,如果可以(简单可靠地)安排对象比使用它的所有对象都长寿。
大多数其他答案都涵盖了这一点,但@T.C.链接到一些非常好的指南,我想在这里总结一下:
工厂功能
默认情况下,生成引用类型的工厂应返回
unique_ptr
,如果要与工厂共享所有权,则应返回shared_ptr
。 -- GotW #90
正如其他人指出的那样,如果您愿意,您作为unique_ptr
的接收者可以将其转换为shared_ptr
。
函数参数
不要将智能指针作为函数参数传递,除非要使用或操作智能指针本身,例如共享或转让所有权。 更喜欢按值、
*
或&
传递对象,而不是通过智能指针。 -- GotW #91
这是因为当您通过智能指针时,您会在函数开始时递增引用计数器,并在结束时递减它。 这些是原子操作,需要跨多个线程/处理器进行同步,因此在高度多线程的代码中,速度损失可能相当高。
当您在函数中时,对象不会消失,因为调用者仍然持有对它的引用(并且在函数返回之前无法对对象执行任何操作),因此,如果您不打算在函数返回后保留对象的副本,则增加引用计数毫无意义。
对于不获取对象所有权的函数:
如果需要表示 null(无对象),请使用
*
,否则更喜欢使用&
;如果对象是仅输入的,则写入const widget*
或const widget&
。 -- GotW #91
这不会强制调用方使用特定的智能指针类型 - 任何智能指针都可以转换为普通指针或引用。 因此,如果您的函数不需要保留对象的副本或获取对象的所有权,请使用原始指针。 如上所述,对象不会在函数的中间消失,因为调用者仍然持有它(除非在特殊情况下,如果这对您来说是一个问题,您已经知道了。
对于获取对象所有权的函数:
使用按值
unique_ptr
参数表示"接收器"函数。
void f( unique_ptr<widget> );
-- GotW #91
这清楚地表明该函数拥有对象的所有权,并且可以将您可能从旧代码中获得的原始指针传递给它。
对于获取对象共享所有权的函数:
表示函数将使用按值
shared_ptr
参数存储和共享堆对象的所有权。 -- GotW #91
我认为这些准则非常有用。 阅读引用来自的页面以获取更多背景和深入的解释,这是值得的。
在大多数情况下,我会按值返回unique_ptr
。大多数资源不应该共享,因为这很难推理它们的生命周期。您通常可以这样编写代码以避免共享所有权。无论如何,您可以从unique_ptr
进行shared_ptr
,因此您不会限制选择。