четверг, 19 ноября 2009 г.

[comp.prog.thoughts] Гримасы вирусной константности

В второй версии языка 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 комментария:

Kodt комментирует...

В С++ глубокая константность членов ингибруется ключевым словом mutable.
А случай с Poko::SharedPtr - это случай дебилизма разработчиков библиотеки.
Поскольку традиционно поведение умных указателей делается аналогичным поведению голых указателей - с их поверхностной константностью, было бы разумно ожидать, что *(const SharedPtr) неконстантно. Но С++ слишком гибок и силён, чтобы родить противоречивые во всех смыслах программы... Вот и получили то, что получили.
Между прочим, глубокая константность указателей имеет смысл, когда мы реализуем агрегат, размещаемый на стороне. И средствами С++ легко сделать распространение на него константности - например, использовать Poko::SharedPtr вместо boost::shared_ptr :)))

eao197 комментирует...

Никогда не пользовался boost::shared_ptr, поэтому с удивлением для себя обнаружил, что там operator* и operator-> не константные.