Kiedy nauczyłem się C ++ dawno temu, mocno podkreślono, że część C ++ polega na tym, że podobnie jak pętle mają „niezmienniki pętli”, klasy mają również niezmienniki związane z czasem życia obiektu - rzeczy, które powinny być prawdziwe tak długo, jak obiekt żyje. Rzeczy, które powinny zostać ustalone przez konstruktorów i zachowane metodami. Hermetyzacja / kontrola dostępu pomaga egzekwować niezmienniki. RAII to jedna z rzeczy, które możesz zrobić z tym pomysłem.
Od C ++ 11 mamy teraz semantykę ruchów. W przypadku klasy, która obsługuje przenoszenie, przenoszenie z obiektu nie kończy formalnie jego żywotności - ruch powinien pozostawić go w jakimś „prawidłowym” stanie.
Czy przy projektowaniu klasy jest to zła praktyka, jeśli projektuje się ją tak, aby niezmienniki klasy były zachowane tylko do momentu, w którym zostały przeniesione? A może to w porządku, jeśli pozwoli ci to przyspieszyć.
Aby uczynić to konkretnym, załóżmy, że mam typ zasobu, którego nie można kopiować, ale można go przenosić:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
I z jakiegokolwiek powodu muszę wykonać kopiowalne opakowanie dla tego obiektu, aby można go było użyć, być może w jakimś istniejącym systemie wysyłkowym.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
W tym copyable_opaque
obiekcie niezmiennikiem klasy ustanowionej podczas budowy jest to, że element członkowski o_
zawsze wskazuje poprawny obiekt, ponieważ nie ma domyślnego ctor, a jedyny ctor, który nie jest ctor kopiującym, gwarantuje to. Wszystkie operator()
metody zakładają, że ten niezmiennik utrzymuje i zachowuje go później.
Jeśli jednak obiekt zostanie przeniesiony, wówczas nie o_
będzie wskazywać na nic. A po tym punkcie wywołanie dowolnej z metod operator()
spowoduje awarię UB /.
Jeśli obiekt nigdy nie zostanie przeniesiony, niezmiennik zostanie zachowany aż do wywołania dtor.
Załóżmy, że hipotetycznie napisałem tę klasę, a kilka miesięcy później mój wymyślony współpracownik doświadczył UB, ponieważ w jakiejś skomplikowanej funkcji, w której z jakiegoś powodu tasowano wiele tych obiektów, odszedł od jednej z tych rzeczy, a później nazwał jedną z jego metody. Najwyraźniej to jego wina pod koniec dnia, ale czy ta klasa jest „źle zaprojektowana?”
Myśli:
W C ++ jest to zwykle zła forma tworzenia obiektów zombie, które eksplodują po ich dotknięciu.
Jeśli nie możesz skonstruować jakiegoś obiektu, nie możesz ustalić niezmienników, to rzuć wyjątek od ctor. Jeśli nie możesz zachować niezmienników w jakiejś metodzie, to w jakiś sposób zasygnalizuj błąd i wycofaj. Czy powinno być inaczej w przypadku obiektów przeniesionych?Czy wystarczy tylko udokumentować „po przeniesieniu tego obiektu, nielegalne (UB) jest robić z nim cokolwiek innego niż zniszczenie” w nagłówku?
Czy lepiej jest stale twierdzić, że jest poprawna w każdym wywołaniu metody?
Tak jak:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Twierdzenia nie poprawiają zasadniczo zachowania i powodują spowolnienie. Jeśli twój projekt używa schematu „kompilacja wydania / kompilacja debugowania”, a nie tylko zawsze działa z asercjami, myślę, że jest to bardziej atrakcyjne, ponieważ nie płacisz za kontrole w kompilacji wersji. Jeśli tak naprawdę nie masz kompilacji debugowania, wydaje się to dość nieatrakcyjne.
- Czy lepiej jest uczynić klasę kopiowalną, ale nie ruchomą?
Wydaje się to również złe i powoduje pogorszenie wydajności, ale rozwiązuje problem „niezmienności” w prosty sposób.
Jakie według Ciebie są tutaj „najlepsze praktyki”?