I have a wrapper to some piece of legacy code.
class A{
L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
A(A const&) = delete;
L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
... // proper resource management here
};
In this legacy code, the function that “duplicates” an object is not thread safe (when calling on the same first argument), therefore it is not marked const
in the wrapper. I guess following modern rules: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
This duplicate
looks like a good way to implement a copy constructor, except for the detail that is it not const
. Therefore I cannot do this directly:
class A{
L* impl_; // the legacy object has to be in the heap
A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
So what is the way out this paradoxical situation?
(Let's say also that legacy_duplicate
is not thread-safe but I know leaves the object in the original state when it exits. Being a C-function the behavior is only documented but has no concept of constness.)
I can think of many possible scenarios:
(1) One possibility is that there is no way to implement a copy constructor with the usual semantics at all. (Yes, I can move the object and that is not what I need.)
(2) On the other hand, copying an object is inherently non-thread-safe in the sense that copying a simple type can find the source in an half-modified state, so I can just go forward and do this perhaps,
class A{
L* impl_;
A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(3) or even just declare duplicate
const and lie about thread safety in all contexts. (After all the legacy function doesn't care about const
so the compiler will not even complain.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Finally, I can follow the logic and make a copy-constructor that takes a non-const argument.
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
It turns out that this works in many contexts, because these objects are not usually const
.
The question is, it this a valid or common route?
I cannot name them, but I intuitively expect lots of problems down the road of having a non-const copy constructor. Probably it will not qualify as a value-type because of this subtlety.
(5) Finally, although this seems to be an overkill and could have a steep runtime cost, I could add a mutex:
class A{
L* impl_;
A(A const& other) : L{other.duplicate_locked()}{}
L* duplicate(){
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
L* duplicate_locked() const{
std::lock_guard<std::mutex> lk(mut);
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
mutable std::mutex mut;
};
But being forced to do this looks like pessimization and makes the class bigger. I am not sure. I am currently leaning towards (4), or (5) or a combination of both.
EDIT 1:
Another option:
(6) Forget about all the non-sense of the duplicate member function and simply call legacy_duplicate
from the constructor and declare that the copy constructor is not thread safe. (And if necessary make another thread-safe versión of the type, A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDIT 2:
This could be a good model for what the legacy function does. Note that by touching the input the call is not thread safe with respect to the value represented by the first argument.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
EDIT 3:
I lately learned that std::auto_ptr
had a similar problem of having a non-const "copy" constructor. The effect was that auto_ptr
couldn't be used inside a container. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/
L
which is modified by creating a newL
instance? If not, why do you believe that this operation is not thread-safe? – Eitenlegacy_duplicate
cannot be called with the same first argument from two different threads. – Fridgeconst
really means. :-) I wouldn't think twice about taking aconst&
in my copy ctor as long as I don't modifyother
. I always think of thread safety as something one adds on top of whatever needs to be accessed from multiple threads, via encapsulation, and I'm really looking forward to the answers. – Ruyle