shared_ptr真实生活用例

时间:2018-02-16 20:19:26

标签: c++ c++11 shared-ptr

当我们有一个需要拥有动态分配项目的多个所有者的场景时,将使用shared_ptr。

问题是,我无法想象我们需要多个所有者的情况。我可以使用unique_ptr来解决我可以成像的每个用例。

有人可以提供一个真实的用例示例,其中包含需要shared_ptr的代码(根据需要,我的意思是作为智能指针的最佳选择)?并且通过"现实生活"我的意思是一些实用和务实的用例,而不是过于抽象和虚构的东西。

9 个答案:

答案 0 :(得分:7)

在我们的模拟器产品中,我们使用框架在模拟组件(称为端点)之间传递消息。这些端点可以驻留在进程内的多个线程上,甚至可以驻留在模拟集群中的多台机器上,其中消息通过RDMA或TCP连接网络进行路由。 API看起来大致如下:

class Endpoint {
public:
    // Fill in sender address, etc., in msg, then send it to all
    // subscribers on topic.
    void send(std::unique_ptr<Message> msg, TopicId topic);

    // Register this endpoint as a subscriber to topic, with handler
    // called on receiving messages on that topic.
    void subscribe(TopicId topic,
        std::function<void(std::shared_ptr<const Message>)> handler);
};

通常,一旦发送方端点执行send,它就不需要等待来自任何接收方的任何响应。因此,如果我们要在整个邮件路由过程中尝试保留单个所有者,那么将所有权保留在send或其他方式的调用方中是没有意义的,send 必须等到所有接收器都完成处理消息,这将引入不必要的往返延迟。另一方面,如果多个接收者订阅topic,那么将唯一所有权分配给他们中的任何一个也是没有意义的,因为我们不知道他们中的哪一个需要消息的时间最长。这将使路由基础设施本身成为唯一的所有者;但同样,在这种情况下,路由基础设施必须等待所有接收器完成,而基础设施可能有许多消息要传递给多个线程,并且它还希望能够将消息传递给接收器和能够转到下一条要传递的消息。另一种方法是保留一组唯一的指针,指向等待线程处理它们的消息,并让接收方在它们完成时通知消息路由器;但这也会带来不必要的开销。

另一方面,通过在此处使用shared_ptr,一旦路由基础结构完成将消息传递到端点的传入队列,它就可以释放所有权以在各个接收器之间共享。然后,线程安全引用计数确保一旦所有接收器完成处理它就会释放Message。并且在远程机器上有订户的情况下,序列化和传输组件可以是消息的另一个共享所有者,同时它正在进行其工作;然后,在接收机器上,接收和反序列化组件可以将它创建的Message副本的所有权传递给该机器上接收者的共享所有权。

答案 1 :(得分:4)

在CAD应用程序中,当多个模型恰好具有相同的网格时(例如,在用户复制粘贴这些模型之后),我使用shared_ptr来保存RAM和VRAM。作为奖励,多个线程可以同时访问网格,因为如果正确使用,shared_ptr和weak_ptr都是线程安全的。

下面是一个简单的例子。真正的代码更复杂,原因很多(GPU缓冲区,鼠标选择,某些用户输入触发的后台处理等等),但我希望这足以让你知道shared_ptr的合理性。

// Can be hundreds of megabytes in these vectors
class Mesh
{
    std::string name;
    std::vector<Vector3> vertices;
    std::vector<std::array<uint32_t, 3>> indices;
    BoundingBox bbox;
};

// Just 72 or 80 bytes, very cheap to copy.
// Can e.g. pass copies to another thread for background processing.
// A scene owns a collection of these things.
class Model
{
    std::shared_ptr<Mesh> mesh;
    Matrix transform;
};

答案 2 :(得分:1)

在类C的成员函数f中调用任何lambda,在那里你要处理一个你将作为参考传递给lambda [&amp;]的对象。当你在f里面等待lambda完成时,C超出了范围。功能消失了,你有一个悬空参考。当lambda接下来访问引用时,分段错误与定义的行为一样接近。你无法将独特的下注者传递给lambda。移动后,您无法从f访问它。解决方案:共享指针和[=]。我编写数据库的核心。我们需要在多线程基础架构中始终使用共享指针。不要忘记原子参考计数器。但是你的普遍怀疑是值得赞赏的。当一个人不需要时,几乎总是使用共享的下注者。

答案 3 :(得分:1)

在我的程序的用户界面中,我有“控制点值”的概念(一个control point value表示我的程序控制的硬件上的控件的当前状态),以及(当然)“概念”小部件“(widget是一个GUI组件,它将控制点的当前状态呈现给监视器,供用户查看和/或操作)。

由于它是一个需要控制的非常精细的系统,我们有

  • 许多不同类型的control point values(浮点数,整数,字符串,布尔值,二进制blob等)
  • 许多不同类型的widget(文字显示,推子,米,旋钮,按钮等)
  • 给定widget可以选择将特定control point value呈现为文本的大量不同方式(大写,小写,更多或更少的精度数字等)

如果我们只是做了显而易见的事情并且每次我们需要上面的新组合时都写了一个新的子类,我们最终会出现数千个子类的几何爆炸,因此会有一个非常大的代码库很难理解或维护。

为了避免这种情况,我将“如何以某种特定方式将control point value翻译成人类可读文本”的知识分离到其自己的单独的不可变对象中,任何人都可以使用它来进行翻译,例如

// My abstract interface
class IControlPointToTextObject
{
public:
   virtual std::string getTextForControlPoint(const ControlPoint & cp) const = 0;
};

// An example implementation
class RenderFloatingPointValueAsPercentage : public IControlPointToTextObject
{
public:
   RenderFloatingPointValueAsPercentage(int precision) : m_precision(precision)
   {
      // empty
   }

   virtual std::string getTextForControlPoint(const ControlPoint & cp) const = 0
   {
      // code to create and return a percentage with (m_precision) digits after the decimal point goes here....
   }

private:
   const int m_precision;
};

......到目前为止,这么好;现在例如当我想要一个文本小部件将控制点值显示为小数点后3位数的百分比时,我可以这样做:

TextWidget * myTextWidget = new TextWidget;
myTextWidget->setTextRenderer(std::unique_ptr<IControlPointToTextObject>(new RenderFloatingPointValueAsPercentage(3)));

......我得到了我想要的东西。但是我的GUI可以变得相当复杂,并且它们可能有大量(数千)小部件,并且使用上述方法我将不得不为每个小部件创建单独的RenderFloatingPointValueAsPercentage对象,即使大多数RenderFloatingPointValueAsPercentage对象最终会被彼此相同。这有点浪费,所以我改变我的widget类来接受std :: shared_ptr,现在我可以这样做:

std::shared_ptr<IControlPointToTextObject> threeDigitRenderer = std::make_shared<RenderFloatingPointValueAsPercentage>(3);

myWidget1->setTextRenderer(threeDigitRenderer);
myWidget2->setTextRenderer(threeDigitRenderer);
myWidget3->setTextRenderer(threeDigitRenderer);
[...]

不用担心对象生命周期,没有悬空指针,没有内存泄漏,没有不必要的重复渲染器对象的创建。 C'est bon:)

答案 4 :(得分:1)

假设我想为一种语言实现GLR parser,该语言是或包含一个递归的&#34;表达式&#34;定义。并且解析不仅要检查输入是否符合语法,还要输出可用于进行分析,评估,编译等的内容。我需要一些东西来表示每个表达式或子表达式语法的结果。符号。每个语法规则的实际语义含义可以用多态来表示,因此这需要是指向基类Expression的某种指针。

然后自然表示为std::shared_ptr<Expression>Expression对象可以是另一个化合物Expression的子表达式,在这种情况下,化合物Expression是子表达式的所有者。或者,Expression对象可以由正在进行的算法的解析堆栈拥有,用于尚未与其他部分组合的语法生成。但实际上并非两者兼而有之。如果我正在编写LALR解析器,我可以使用std::unique_ptr<Expression>,将子表达式从解析堆栈传递到复合表达式构造函数,因为每个语法符号都会减少。

shared_ptr的特定需求提出了GLR算法。在某些点,当到目前为止扫描的输入存在多于一个可能的解析时,算法将复制解析堆栈以尝试每种可能性的暂定解析。并且随着暂定解析的进行,每个可能性都可能需要使用其自己的解析堆栈中的一些中间结果来形成某些复合表达式的子表达式,所以现在我们可能会使用相同的Expression解析堆栈和一些不同的复合Expression对象。希望除了一个暂定的解析之外的所有解析最终都会失败,这意味着失败的解析堆栈将被丢弃。被丢弃的解析堆栈直接和间接包含的Expression对象应该在那时被销毁,但其中一些可能被其他解析堆栈直接或间接使用。

只用std::unique_ptr就可以做到这一切,但要复杂得多。每当解析堆栈需要拆分时,你就可以进行深度克隆,但这可能是浪费。你可以让它们拥有一些其他主容器并让解析堆栈和/或复合表达式只使用哑指针,但知道何时清理它们将是困难的(并且最终可能会复制{{的简化实现1}})。我认为std::shared_ptr在这里是明显的赢家。

答案 5 :(得分:1)

看到这个现实生活中的例子。当前帧在多个消费者之间共享,并且智能指针变得容易。

class frame { };

class consumer { public: virtual void draw(std::shared_ptr<frame>) = 0; };

class screen_consumer_t :public consumer { public:  void draw(std::shared_ptr<frame>) override {} };
class matrox_consumer_t :public consumer { public:  void draw(std::shared_ptr<frame>) override {} };
class decklink_consumer_t :public consumer { public:  void draw(std::shared_ptr<frame>) override {} };

int main() {
    std::shared_ptr<frame> current_frame = std::make_shared<frame>();

    std::shared_ptr<consumer> screen_consumer = std::make_shared<screen_consumer_t>();
    std::shared_ptr<consumer> matrox_consumer = std::make_shared<matrox_consumer_t>();
    std::shared_ptr<consumer> decklink_consumer = std::make_shared<decklink_consumer_t>();

    std::vector<consumer> consumers;
    consumers.push_back(screen_consumer);
    consumers.push_back(matrox_consumer);
    consumers.push_back(decklink_consumer);

    //screen_consumer->draw(current_frame);
    //matrox_consumer->draw(current_frame);
    //decklink_consumer->draw(current_frame);

    for(auto c: consumers) c->draw(current_frame);


}

编辑:

另一个例子可以是Minimax树,为了避免循环冗余,可以使用weak_ptr和shared_ptr:

struct node_t
{
    std::unique_ptr<board_t> board_;
    std::weak_ptr<node_t> parent_;
    std::vector<std::shared_ptr<node_t>> children_;
};

答案 6 :(得分:1)

您是否检查过这些关于写时复制载体的文章:

https://iheartcoding.net/blog/2016/07/11/copy-on-write-vector-in-c/

copy-on-write PIMPL

https://crazycpp.wordpress.com/2014/09/13/pimplcow/

和通用写时复制指针

https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Copy-on-write

所有这些都在内部使用#sidebar { position: relative !important; top: 0 !important; left: 0% !important; }

答案 7 :(得分:0)

std::shared_ptr是C ++中reference counting技术的实现。有关引用计数的用例,请参阅链接的维基百科文章。引用计数的一种用法是编程语言中的garbage collection。因此,如果您决定使用C ++编写一个带有垃圾收集的新编程语言,您可以使用std::shared_ptr实现它,尽管您还必须处理周期。

答案 8 :(得分:0)

简单地说:实际上并没有。

有关更详细的解释,让我们转向正式推理。众所周知,C ++是图灵完备的确定性语言。一个流行的简单例子,同样具有计算功能的强大工具是Brainfuck(通常非常方便地建立你最喜欢的语言的图灵完整性)。如果我们看一下Brainfuck的描述(这确实非常小,这使得它对于前面提到的目的非常方便),我们很快就会发现在那里没有任何类似于shared_ptr的概念。所以答案是:不,没有一个真实的例子,他们绝对需要。所有可计算的都可以在没有shared_ptrs的情况下完成。

如果我们彻底地继续这个过程,我们将同样容易地摆脱其他不必要的概念,即unique_ptr,std :: unordered_map,exception,range-loops等等。