Tengo un contenedor para alguna pieza de código heredado.
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
};
En este código heredado, la función que "duplica" un objeto no es segura para subprocesos (al invocar el mismo primer argumento), por lo tanto, no está marcada const
en el contenedor. Supongo que seguir las reglas modernas: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Esto duplicate
parece una buena manera de implementar un constructor de copia, excepto por el detalle que no es const
. Por lo tanto, no puedo hacer esto directamente:
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;}
};
Entonces, ¿cuál es la salida de esta situación paradójica?
(Digamos también que legacy_duplicate
no es seguro para subprocesos, pero sé que deja el objeto en el estado original cuando sale. Al ser una función C, el comportamiento solo está documentado pero no tiene un concepto de const.)
Puedo pensar en muchos escenarios posibles:
(1) Una posibilidad es que no hay forma de implementar un constructor de copia con la semántica habitual. (Sí, puedo mover el objeto y eso no es lo que necesito).
(2) Por otro lado, copiar un objeto es inherentemente no seguro para subprocesos en el sentido de que copiar un tipo simple puede encontrar la fuente en un estado medio modificado, por lo que puedo seguir adelante y hacer esto quizás,
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) o incluso simplemente declarar duplicate
const y mentir sobre la seguridad del hilo en todos los contextos. (Después de todo, la función heredada no se preocupa por const
lo que el compilador ni siquiera se quejará).
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Finalmente, puedo seguir la lógica y hacer un constructor de copias que tome un argumento no constante .
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Resulta que esto funciona en muchos contextos, porque estos objetos no suelen serlo const
.
La pregunta es, ¿es esta una ruta válida o común?
No puedo nombrarlos, pero intuitivamente espero muchos problemas en el futuro de tener un constructor de copia no constante. Probablemente no calificará como un tipo de valor debido a esta sutileza.
(5) Finalmente, aunque esto parece ser una exageración y podría tener un alto costo de tiempo de ejecución, podría agregar un 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;
};
Pero verse obligado a hacer esto parece pesimismo y hace que la clase sea más grande. No estoy seguro. Actualmente me estoy inclinando hacia (4) o (5) o una combinación de ambos.
—— EDITAR
Otra opción:
(6) Olvídese de todo el sentido de la función miembro duplicada y simplemente llame legacy_duplicate
desde el constructor y declare que el constructor de copia no es seguro para subprocesos. (Y si es necesario, cree otra versión del tipo segura para subprocesos A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDITAR 2
Este podría ser un buen modelo para lo que hace la función heredada. Tenga en cuenta que al tocar la entrada, la llamada no es segura para subprocesos con respecto al valor representado por el primer argumento.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
legacy_duplicate
no se puede invocar con el mismo primer argumento de dos hilos diferentes.
const
realmente significa. :-) No lo pensaría dos veces antes de tomar un const&
copiador en mi copiador, siempre y cuando no lo modifique other
. Siempre pienso en la seguridad de los subprocesos como algo que se agrega además de cualquier cosa a la que se deba acceder desde múltiples subprocesos, mediante encapsulación, y realmente estoy esperando las respuestas.
L
cual se modifica creando una nuevaL
instancia? Si no es así, ¿por qué cree que esta operación no es segura para subprocesos?