В процессе одного из споров на RSDN-е родился пример того, как компилятор, применив NRVO, приводит к неожиданному поведению.
Под катом полный код примера. Пока же пару слов о том, на что смотреть.
Есть простой класс entity, который содержит внутри себя целочисленное значение. Изменить это целочисленное значение можно только вызывав метод entity::update. Но это не const-метод, т.е. для константного entity его вызывать нельзя.
Фокус с entity в том, что есть еще и entity_registry, т.е. реестр объектов entity. Если в конструктор entity передать ссылку на entity_registry, то entity себя в реестре зарегистрирует. А в деструкторе -- вычеркнет.
Пока объект находится в реестре, его можно изменить через реестр: вызываем метод entity_registry::update и реестр вызывает update для всех зарегистрированных в нем объектов.
Но важно отметить, что если объект entity создавался без передачи ему в конструктор ссылки на реестр, то он ни в каких реестрах не регистрируется, поэтому через реестр его изменить нельзя. Так что в такой ситуации:
void check() { entity_registry registry; entity e1{registry}; entity e2; // Не в реестре. entity e3{e1}; // Не в реестре. registry.update(25); std::cout << e1.value() << ", " << e2.value() << ", " << e3.value() << std::endl; } |
Мы получим вполне ожидаемое:
add to registry: 0x7ffd7b567c18
25, 0, 0
removed from registry: 0x7ffd7b567c18
Т.е. объект e1 добавил себя в реестр, потом мы получили три предсказуемых значения (два нулевых, т.к. никто не менял e2 и e3), затем e1 изъял себя из реестра.
А теперь внимание, вопрос: что будет во в таком случае?
entity make_entity(entity_registry & registry) { entity ent{registry}; ent.update(42); return ent; } void demo() { entity_registry registry; const entity ent = make_entity(registry); registry.update(100); std::cout << ent.value() << std::endl; } |
Можем ли мы изменть значение константного ent внутри функции demo через вызов update у реестра?
Да. Сможем.
Все дело в том, что компилятор применит для возвращенного из make_entity значения оптимизацию NRVO и, фактически, в demo под видом константного ent продолжит жить неконстантный ent из make_entity, который все еще зарегистрирован в реестре.
Хотя, если бы NRVO здесь не применялась бы, то объект ent из make_entity должен был бы разрушится и вычеркнуть себя из реестра, а объект ent из demo был бы самой настоящей константой, про которую реестр не знает.
Но т.к. NRVO (RVO) плевать на побочные эффекты выбрасываемых конструкторов и деструкторов, то имеем что имеем.
Да, а UB здесь в том, что мы умудряемся менять константный ent в demo, а снимать константность с константного объекта в C++ -- это UB.
Таки да, пример синтетический. Хотя это как посмотреть. Помещение объекта в какой-то реестр не такая уж и редкая ситуация.
Попутно можно сказать еще и о том, что во время работы конструкторов-деструкторов у объекта константности нет (т.е. там есть неконстантный указатель this). Что позволяет нам сделать так:
void stupid() { entity_registry registry; const entity immutable_ent{registry}; ... } |
Т.е. константный объект окажется зарегистрирован в реестре и в этом реестре окажется неконстантный указатель на константный объект. Со всеми вытекающими...
Но в этом-то случае мы сами себе злобные буратино. Тогда как в показанном ниже коде в make_entity и demo в этом плане все нормально. Мы своими руками константные объекты в реестр не засовывали. Здесь нам компилятор "подсобил"...
#include <iostream> #include <set> class entity; class entity_registry; class entity { entity_registry * registry_{}; int value_{}; public: entity() = default; entity(entity_registry & registry); entity(const entity & source); entity(entity && source); ~entity(); entity & operator=(entity &&) = delete; entity & operator=(const entity &) = delete; int value() const noexcept; void update(int delta); }; class entity_registry { std::set<entity *> entities_; public: void add(entity & ent) { entities_.insert(&ent); std::cout << "add to registry: " << &ent << std::endl; } void remove(entity & ent) { entities_.erase(&ent); std::cout << "removed from registry: " << &ent << std::endl; } void update(int delta) { for(auto * ent : entities_) ent->update(delta); } }; entity::entity(entity_registry & registry) : registry_{®istry} { registry_->add(*this); } entity::entity(const entity & source) : value_{source.value_} { } entity::entity(entity && source) : value_{source.value_} { } entity::~entity() { if(registry_) registry_->remove(*this); } int entity::value() const noexcept { return value_; } void entity::update(int delta) { value_ += delta; } void consume_one(const entity & ent) { std::cout << "consume_one: " << ent.value() << std::endl; } void consume_two(const entity & ent) { std::cout << "consume_two: " << ent.value() << std::endl; } entity make_entity(entity_registry & registry) { entity ent{registry}; ent.update(42); return ent; } void demo() { entity_registry registry; const entity ent = make_entity(registry); std::cout << "value: " << ent.value() << std::endl; consume_one(ent); registry.update(100); consume_two(ent); } int main() { demo(); } |
Результат работы (GCC-11, Clang-16):
add to registry: 0x7ffd51ed71f0
value: 42
consume_one: 42
consume_two: 142
removed from registry: 0x7ffd51ed71f0
Комментариев нет:
Отправить комментарий