Search code examples
c++shared-ptrc++17

Owning or sharing a resource between objects


A have a collection of worker objects with the following type:

template<class T> struct Worker;

Different workers, having different template arguments T, use some resource

struct Resource;

Resources used by different Workers all have the same type but possibly different values. Resources are heavy in general, unnecessary copies should be avoided. Resources are used only by workers, some are used exclusively, some are shared between them (this information is available at compile-time). If a resource is used exclusively, I want a Worker to be (possibly, effectively) the owner of it, otherwise it should store a reference. In pseudo-code, I want to do either

Resource resource1(...);
Worker<Type1> worker1(..., std::move(resource1));
// worker1 owns resource1

Resource resource2(...);
Worker<Type2> worker2(..., std::move(resource2));
// worker2 owns resource2

or

Resource resource(...);
Worker<Type1> worker1(..., resource);
Worker<Type2> worker2(..., resource);
// workers use resource, but do not own it

I see two direct ways to do it:

  1. Use a template parameter to determine the type of the data member in Worker that represents a resource:

    template<class T, class R>
    struct Worker
    {
        Worker(T arg, R resource) :
            resource_(std::forward<R>(resource))
        { }  
    
        R resource_;
    };
    
    template<class T, class R>
    Worker(T arg, R&& resource) -> Worker<T, R>;
    
    void foo()
    {
        Resource r;
    
        Worker worker1(0, r);
        Worker worker2(0, r);
        // type of worker1.resource_ is Resource&,
        // type of worker2.resource_ is Resource&,
        // both store a reference to r
    
        Worker worker3(0, Resource{});
        // type of worker3.resource_ is Resource,
        // Resource{} is moved into it
    }
    
  2. Use std::shared_ptr:

    template<class T>
    struct Worker
    {
        Worker(T arg, const std::shared_ptr<Resource>& resource) :
            resource_(resource)
        { }
    
        template<class R, typename = std::enable_if_t<std::is_same_v<R, Resource>>>
        Worker(T arg, R&& resource) :
            resource_(std::make_shared<Resource>(std::move(resource)))
        { }
    
        std::shared_ptr<Resource> resource_;
    };
    
    void foo()
    {
        auto resource = std::make_shared<Resource>();
    
        Worker worker1(0, resource);
        Worker worker2(0, resource);
        // worker1 and worker2 share *resource_
    
        Worker worker3(0, Resource{});
        // worker3 effectively owns *resource_
    }
    

These approaches are obviously very different but are effectively equivalent in present case. I don't like them both.

In the first case I don't like that the resource is stored outside Workers in some local variable. There is a potential risk to get a dangling reference in the Worker.

In the second case unnecessary counted reference remains in resource after all Workers have been created. I could take shared_ptr by value and use std::move in the last Worker construction, but it would require me to track the last constructor explicitly - an excellent way to get a bug.

These concerns do not influence the correctness of the code, but probably signify that the whole design is flawed. I think there should be much better approach. Please explain what are the best practices in such cases. (I understand that my question may be too generic, and the solution I need may depend on the exact way I create resources and workers; good solution may result from a different view on the problem, and currently I'm stuck in the two approaches I outlines above.)

Full code here.


Solution

  • In the second case unnecessary counted reference remains in resource after all Workers have been created. I could take shared_ptr by value and use std::move in the last Worker construction, but it would require me to track the last constructor explicitly - an excellent way to get a bug.

    No, you don't; Create your resources with unique_ptr by default, it costs "nothing" to convert it to a shared_ptr when needed.


    Consider using only a constructor that takes a std::shared_ptr by value; std::shared_ptr has a constructor that takes an rvalue std::unique_ptr;

    template<class T>
    struct Worker
    {
        Worker(T arg, std::shared_ptr<Resource> resource) :
            resource_(std::move(resource))
        { }
    
        std::shared_ptr<Resource> resource_;
    };
    
    void foo()
    {
        auto resource = std::make_unique<Resource>();
        auto resource_shared = std::make_shared<Resource>();
    
        Worker worker1(0, std::move(resource));
        Worker worker2(0, resource_shared);
        Worker worker3(0, resource_shared);
        // worker2 and worker3 share *shared_resource
    
    
        Worker worker4(0, std::move(resource));  //resource is empty
    }
    

    Live Demo