Ho un wrapper per qualche codice legacy.
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 questo codice legacy, la funzione che "duplica" un oggetto non è thread-safe (quando si chiama lo stesso primo argomento), quindi non è contrassegnata const
nel wrapper. Immagino che seguano le regole moderne: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Questo duplicate
sembra un buon modo per implementare un costruttore di copie, ad eccezione del dettaglio che non lo è const
. Pertanto non posso farlo direttamente:
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;}
};
Qual è la via d'uscita da questa situazione paradossale?
(Diciamo anche che legacy_duplicate
non è thread-safe ma so che lascia l'oggetto nello stato originale quando esce. Essendo una funzione C il comportamento è solo documentato ma non ha un concetto di costanza.)
Posso pensare a molti possibili scenari:
(1) Una possibilità è che non vi sia alcun modo di implementare un costruttore di copie con la solita semantica. (Sì, posso spostare l'oggetto e non è quello di cui ho bisogno.)
(2) D'altra parte, la copia di un oggetto è intrinsecamente non thread-safe, nel senso che la copia di un tipo semplice può trovare l'origine in uno stato semi-modificato, quindi posso semplicemente andare avanti e farlo forse,
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 addirittura dichiarare duplicate
const e mentire sulla sicurezza dei thread in tutti i contesti. (Dopo tutto la funzione legacy non si preoccupa, const
quindi il compilatore non si lamenterà nemmeno.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Infine, posso seguire la logica e creare un costruttore di copie che accetta un argomento non const .
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Si scopre che questo funziona in molti contesti, perché questi oggetti di solito non lo sono const
.
La domanda è: si tratta di un percorso valido o comune?
Non posso nominarli, ma intuitivamente mi aspetto molti problemi lungo la strada di avere un costruttore di copie non const. Probabilmente non si qualificherà come un tipo di valore a causa di questa sottigliezza.
(5) Infine, anche se questo sembra essere eccessivo e potrebbe avere un costo di runtime elevato, potrei aggiungere 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;
};
Ma essere costretti a farlo sembra una pessimizzazione e rende la classe più grande. Non sono sicuro. Attualmente mi sposto verso (4) , o (5) o una combinazione di entrambi.
—— EDIT
Un'altra opzione:
(6) Dimentica tutto il non-senso della funzione membro duplicato e chiama semplicemente legacy_duplicate
dal costruttore e dichiara che il costruttore della copia non è thread-safe. (E se necessario crea un'altra versione thread-safe del tipo, A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
MODIFICA 2
Questo potrebbe essere un buon modello per ciò che fa la funzione legacy. Si noti che toccando l'input la chiamata non è thread-safe rispetto al valore rappresentato dal primo argomento.
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
non può essere chiamata con lo stesso primo argomento da due thread diversi.
const
significhi davvero. :-) Non ci penserei due volte a prendere un const&
nel mio ctor copia finché non modifico other
. Penso sempre alla sicurezza dei thread come qualcosa che si aggiunge a tutto ciò che è necessario accedere da più thread, tramite incapsulamento, e non vedo davvero l'ora di avere le risposte.
L
quale viene modificato creando una nuovaL
istanza? In caso contrario, perché ritieni che questa operazione non sia thread-safe?