Mam opakowanie jakiegoś starszego kodu.
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
};
W tym starszym kodzie funkcja „duplikująca” obiekt nie jest bezpieczna dla wątków (podczas wywoływania tego samego pierwszego argumentu), dlatego nie jest oznaczona const
w opakowaniu. Chyba następujące współczesne zasady: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
To duplicate
wygląda na dobry sposób na implementację konstruktora kopii, z wyjątkiem szczegółów, które nie są const
. Dlatego nie mogę tego zrobić bezpośrednio:
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;}
};
Jakie jest zatem wyjście z tej paradoksalnej sytuacji?
(Powiedzmy również, że legacy_duplicate
nie jest to bezpieczne dla wątków, ale wiem, że opuszcza obiekt w oryginalnym stanie po wyjściu. Jako funkcja C zachowanie jest tylko udokumentowane, ale nie ma pojęcia stałości).
Mogę wymyślić wiele możliwych scenariuszy:
(1) Jedną z możliwości jest to, że nie ma możliwości zaimplementowania konstruktora kopii o zwykłej semantyce. (Tak, mogę przesunąć obiekt i to nie jest to, czego potrzebuję.)
(2) Z drugiej strony, kopiowanie obiektu z natury nie jest bezpieczne w tym sensie, że kopiowanie prostego typu może znaleźć źródło w stanie częściowo zmodyfikowanym, więc mogę po prostu iść do przodu i zrobić to, być może,
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) lub nawet po prostu deklarować duplicate
const i kłamać na temat bezpieczeństwa wątków we wszystkich kontekstach. (Po tym wszystkim, na czym polega starsza funkcja, const
kompilator nawet nie narzeka.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Na koniec mogę postępować zgodnie z logiką i stworzyć konstruktor kopii, który przyjmuje argument nie-stały .
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Okazuje się, że działa to w wielu kontekstach, ponieważ te obiekty zwykle nie są const
.
Pytanie brzmi, czy jest to ważna czy wspólna trasa?
Nie potrafię ich nazwać, ale intuicyjnie oczekuję wielu problemów na drodze do posiadania konstruktora kopii non-const. Prawdopodobnie nie będzie się kwalifikować jako typ wartości z powodu tej subtelności.
(5) Wreszcie, chociaż wydaje się to przesadą i może mieć wysokie koszty w czasie wykonywania, mogę dodać muteks:
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;
};
Ale zmuszenie do zrobienia tego wygląda na pesymalizację i powiększa klasę. Nie jestem pewien. Obecnie skłaniam się w kierunku (4) lub (5) lub kombinacji obu.
EDYCJA 1:
Inna opcja:
(6) Zapomnij o braku zrozumienia funkcji duplikatu elementu i po prostu wywołaj legacy_duplicate
z konstruktora i zadeklaruj, że konstruktor kopiowania nie jest bezpieczny dla wątków. (I jeśli to konieczne, wykonaj kolejną bezpieczną dla wątku wersję tego typu A_mt
).
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
EDYCJA 2:
Może to być dobry model do tego, co robi starsza funkcja. Zauważ, że dotknięcie wejścia powoduje, że wywołanie nie jest bezpieczne dla wątku w odniesieniu do wartości reprezentowanej przez pierwszy 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;
}
EDYCJA 3:
Niedawno dowiedziałem się, że std::auto_ptr
miał podobny problem z konstruktorem „kopiowania” innym niż const. Efekt był taki, że auto_ptr
nie można go użyć w pojemniku. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/
legacy_duplicate
nie może zostać wywołana z tym samym pierwszym argumentem z dwóch różnych wątków.
const
tak naprawdę znaczy. :-) Nie zastanawiałbym się dwa razy nad pobraniem const&
mojego narzędzia kopiowania, dopóki nie zmodyfikuję other
. Zawsze myślę o bezpieczeństwie wątków jako o czymś, co dodaje się do wszystkiego, co wymaga dostępu z wielu wątków, poprzez enkapsulację, i naprawdę nie mogę się doczekać odpowiedzi.
L
którym jest modyfikowany przez utworzenie nowejL
instancji? Jeśli nie, dlaczego uważasz, że ta operacja nie jest bezpieczna dla wątków?