В второй версии языка D хотят сделать т.н. “вирусную константность” – если объект A хранит ссылку на объект B, то в константном методе объекта A объект B так же будет константой. Продемонстрирую это на примере C++ (поскольку с D уже давно не работал и синтаксис подзабыл). Итак, в C++ мы имеем:
class B {
public :
void change() {}
void query() const {};
};
class A {
private :
B b_;
public :
void query() const {
b_.change(); // (1)
b_.query();
}
};
Строка, помеченная (1) не компилируется, т.к. в константном методе A::query() объект b_ так же считается константой. Это потому, что атрибут A::b_ является объектом. Если бы он был ссылкой (или указателем), то для него можно было бы вызывать метод change() без проблем – мы же не меняем атрибут A::b_. Поэтому следующий код компилируется и работает “на ура”:
class B {
public :
void change() {}
void query() const {};
};
class A {
private :
B & b_;
public :
A( B & b ) : b_( b ) {}
void query() const {
b_.change();
b_.query();
}
};
Но это в C++, в котором вирусной константности, вроде бы нет (об этом ниже). А вот в D он не будет работать, т.к. там в методе A::query атрибут A::b_ сменил бы свой тип с B& на const B&.
Вирусная константность стала одной из главных причин, по которым язык D мне уже не интересен. Поскольку неприятности возникают как раз там, где мне бы не хотелось их иметь. В частности, когда я продумывал портирование SObjectizer в D2, я думал, что сообщения между агентами будут ходить в виде иммутабельных объектов. Это позволило бы передавать один объект сообщения сразу нескольким агентам одновременно (в разных потоках) и не задумываться о том, что какой-то агент это сообщение модифицирует – просто не смог бы этого сделать.
Это было бы классной фишкой языка D, но! При этом в сообщении нельзя было бы передавать мутабельные ссылки. Т.е. вообще нельзя. Представьте себе, что ряду агентов нужно разослать сообщение с ссылкой на новый логгер:
class ChangeLoggerMsg : public Msg {
private :
Logger & logger_;
public :
Logger & logger() const { return logger_; }
};
Вроде бы просто. Но в D2 это не пройдет. Поскольку метод ChangeLoggerMsg::logger() не сможет возвратить Logger& – только const Logger&. А константная ссылка на логгер, мягко говоря, на фиг никому не упала.
Этот пример (или аналогичный ему) я приводил в news-группе языка D, пытаясь привлечь внимание к тому, что вирусная константность – это не есть хорошо на 100%. Никого там это не тронуло, ну и ладно. Это уже будут проблемы D-шников.
Но вот на днях довелось столкнуться со случаем вирусной константности в C++. Да, да, именно вирусной константности, я сам удивился. А дело было так: во время code review нашел что-то вроде:
class factory_base_t {
// Интерфейс фабрики для создания некоторых объектов.
// Часть методов фабрики является неконстантными.
};
typedef Poco::SharedPtr< factory_base_t > factory_base_ptr_t;
// Каталог доступных фабрик.
// По списку фабрик можно пройтись, выбрать нужные, и скопировать
// ссылки на них к себе.
std::vector< factory_base_ptr_t > &
query_all_factories() {
static std::vector< factory_base_ptr_t > factories;
// Вектор factories заполняется при первом обращении, а затем
// может меняться время от времени.
return factories;
}
На мой взгляд, плохо здесь то, что получив ссылку на вектор из query_all_factories можно было случайно модифицировать этот вектор. Лучше было бы возвращать константную ссылку на вектор.
Но делать это нельзя. Поскольку, константный operator[] для vector<T> возвращает const T&. В данном случае это будет const SharedPtr<T>. А константный operator* (и operator->) для SharedPtr<T> будет возвращать const T&. Т.е. если возвращать константную ссылку на вектор фабрик, то для фабрик из этого вектора нельзя будет вызывать неконстантные методы. Грубо говоря, нельзя будет написать:
const std::vector< factory_base_ptr_t > & v = query_all_factories();
for( size_t i = 0; i != v.size(); ++i )
v[ i ]->decrement_priority( priority_delta );
Выпутаться из этой ситуации можно разными способами. Например, можно возвращать вектор фабрик по значению. Или можно возвращать объект, который будет хранить в себе ссылку на вектор, но не будет давать к ней доступа. Можно делать копию умного указателя на фабрику перед обращением к ней. Можно и еще что-нибудь сделать – в том числе и полностью сменить дизайн хранилища фабрик (а так же сменить язык программирования, место работы и страну проживания ;).
Однако, важен сам факт того, что нужно вообще что-то делать. Казалось бы, простой случай – я хочу защитить объект от модификации, но не хочу защищать от этого те объекты, на которые он ссылается. Ан нет! :) Не знаю, как кому, но меня раздражает необходимость обходить систему типов в таких случаях.
Наличие константности в языке, по-моему, очень большой плюс. Это как раз то, что меня держит в C++ (помимо прочего). Но вот чрезмерная константность – на мой взгляд, это плохо.
При наличии обычной (не вирусной) константности обеспечить вирусную можно без проблем – достаточно определить константные геттеры, как это делается в C++. А вот когда константность вирусная по умолчанию, то избавиться от нее гораздо сложнее.
2 комментария:
В С++ глубокая константность членов ингибруется ключевым словом mutable.
А случай с Poko::SharedPtr - это случай дебилизма разработчиков библиотеки.
Поскольку традиционно поведение умных указателей делается аналогичным поведению голых указателей - с их поверхностной константностью, было бы разумно ожидать, что *(const SharedPtr) неконстантно. Но С++ слишком гибок и силён, чтобы родить противоречивые во всех смыслах программы... Вот и получили то, что получили.
Между прочим, глубокая константность указателей имеет смысл, когда мы реализуем агрегат, размещаемый на стороне. И средствами С++ легко сделать распространение на него константности - например, использовать Poko::SharedPtr вместо boost::shared_ptr :)))
Никогда не пользовался boost::shared_ptr, поэтому с удивлением для себя обнаружил, что там operator* и operator-> не константные.
Отправить комментарий