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;
Resource
s used by different Worker
s 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:
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
}
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 Worker
s 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 Worker
s 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.)
In the second case unnecessary counted reference remains in resource after all Workers have been created. I could take
shared_ptr
by value and usestd::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
}