Недавно столкнулся с задачей, в которой было бы хорошо иметь транзитивную константность в C++.
Транзитивная константность -- штука своеобразная. И, по большей части, мне нравится, что в C++ ее нет. Но вот оказался в ситуации, когда она была бы в тему.
Что такое транзитивная константность?
Представим себе, что у нас есть:
class Foo { public: void foo(); // Это не-const метод. void foo2() const; // А это уже const-метод. }; class Bar { Foo * m_foo; // Это не-const указатель! public: ... void bar2() const { // Это const-метод, в котором мы не можем присвоить m_foo новое значение. m_foo->foo(); // Но зато можем вызвать не-const метод для m_foo. } }; Foo foo; const Bar bar{&foo}; bar.bar2(); // Этот вызов может изменить foo. |
Из-за того, что в C++ константность не транзитивна, то в const-объекте bar можно иметь не-const указатель на foo и в const-методе Bar::bar2 можно поменять объект foo.
Если бы константность была транзитивной, то в Bar::bar2 указатель Bar::m_foo автоматически бы стал константным и вызвать в Bar::bar2 не-const метод Foo::foo у нас уже не получилось бы.
Поскольку в С++ транзитивной константности нет, то я было попробовал сделать ее вручную. По типу чего-то такого:
template<typename T> class AutoConstPtr { T * m_ptr; public: ... [[nodiscard]] T * get() { return m_ptr; } // Не-const. [[nodiscard]] const T * get() const { return m_ptr; } // Уже const. }; |
Это позволяет получить транзитивную константность в простом случае:
class Bar { AutoConstPtr<Foo> m_foo; // Это уже не raw pointer. public: ... void bar2() const { m_foo.get()->foo(); // А вот здесь будет ошибка компиляции! } }; |
И это уже было именно то, что мне нужно. И, казалось бы, счастье было уже так близко...
Но, к сожалению, это не сработало на практике. Например, из-за вот таких случаев:
void ProcessItems(const std::vector<AutoConstPtr<Foo>> & items) { for(auto p : items) { p.get()->foo(); // Упс! } } |
Фокус в том, что p -- это будет копия AutoConstPtr<Foo>. Не-const копия. Следовательно, для p будет вызываться не-const версия get. Следовательно, будет возвращаться не-const указатель на Foo. Следовательно, можно вызывать не-const методы Foo, т.е. модифицировать Foo. И это в ситуации, когда исходно у нас были как раз константные указатели на Foo (ведь у нас const-ссылка на вектор указателей).
Вот таким вот незамысловатым образом красивая идея накрылась медным тазом. Абыдна, да 🙁
И вот тут мне захотелось, чтобы в C++ при описании конструктора можно было бы явно описать, применяется ли этот конструктор для const-объекта или нет.
Ведь сейчас мы пишем что-то вроде:
MyClass::MyClass(const MyClass & other) {...} |
и понимаем, что это конструктор копирования. Но не понимаем, какой именно экземпляр MyClass при этом конструируется. Т.е.:
MyClass source{...}; MyClass copy1{source}; // Вызов конструктора копирования. const MyClass copy2{source}; // Вызов того же самого конструктора копирования. |
А что, если бы мы могли добавлять const и к конструктору?
template<typename T> class AutoConstPtr { T * m_ptr; public: ... // Здесь какие-то другие конструкторы. AutoConstPtr(const AutoConstPtr &) = delete; // Нельзя построить не-const из const. AutoConstPtr(const AutoConstPtr & other) const // Тут все OK. : m_ptr{other.m_ptr} {} ... }; |
Тогда бы не получилось бы скомпилировать конструкцию:
for(auto p : items) ... |
потому что нельзя построить не-const объект AutoConstPtr из const-объекта.
А вот так бы получилось бы:
for(const auto p : items) ... |
Вот такая вот странная фантазия.
Но это реально фантазия, т.к. даже если бы была возможность помечать конструкторы как const, то непонятно было бы что делать вот с такими ситуациями:
const auto std::vector<AutoConstPtr<Foo>> & source = ...; std::vector<AutoConstPtr<Foo>> selected; std::copy_if(source.begin(), source.end(), std::back_inserter(selected), [](const auto & item) { return ...; }); |
Так что, возможно, идея транзитивной константности в принципе не для C++.
Комментариев нет:
Отправить комментарий