понедельник, 26 июля 2010 г.

Тонкости передачи объектов через константные ссылки в C++

Может быть, для кого-то это это покажется очевидным с первого взгляда. Так вышло, что не для меня. Итак, вернемся к моему предыдущему посту о применении boost::spirit для построения простого пользовательского языка. Для вычисления уже готового AST был создан класс CexmcAST::BasicEval. Вот его определение:

    class  BasicEval
    {
        protected:
            typedef variant< int, double >  ScalarValueType;

        protected:
            virtual ~BasicEval();

        public:
            bool  operator()( const Subtree &  ast ) const;

        protected:
            ScalarValueType          GetScalarValue( const Node &  node ) const;

            virtual ScalarValueType  GetFunScalarValue( const Subtree &  ast )
                                                                        const;

            virtual ScalarValueType  GetVarScalarValue( const Variable &  var )
                                                                        const;

            ScalarValueType          GetBasicFunScalarValue(
                                        const Subtree &  ast, bool &  result )
                                                                        const;
    };
Для раскрытия проблемы нам понадобится также определение типа CexmcAST::Variable:
    struct  Variable
    {
        Variable() : index1 ( 0 ), index2( 0 ), addr( ( const int * ) NULL )
        {}

        std::string                             name;

        int                                     index1;

        int                                     index2;

        variant< const int *, const double * >  addr;
    };
BasicEval::operator() принимает в качестве аргумента константную ссылку на объект типа Subtree. В свою очередь, он вызывает рекурсивную функцию BasicEval::GetScalarValue(), которой передается этот объект. Однако, по определению, GetScalarValue() принимает константную ссылку на объект типа Node, который определяется как вариант:
    typedef variant< Tree, Leaf >          Node;
Наверное, кто-то уже предугадал дальнейший ход событий...
Унаследовавшись от BasicEval, я решил замапить адреса переменных addr объектов типа Variable, которые были созданы в деревьях ParseResult::expression (собственно, эти деревья и передаются через константные ссылки в BasicEval::operator()) в результате вызова phrase_parse(). По замыслу, маппинг происходил при первом вызове BasicEval::operator(), в дальнейшем, для получения значения переменной использовался бы уже замапленный адрес. Маппинг происходил где-то в глубинах вызовов BasicEval::operator() -> BasicEval::GetScalarValue() -> ... . Eстественно, для изменения объектов ParseResult::expression мне пришлось использовать const_cast. Адреса успешно присваивались, но при следующем вычислении выражения они также успешно забывались (снова оказывались неинициализированными).
Прблема (ну это же очевидно!) скрывалась в первом вызове BasicEval::GetScalarValue(). BasicEval::operator() не передавал ей константную ссылку на объект Subtree как я ожидал, вместо этого GetScalarValue() создавал новый объект типа Node, инициализированный переданным объектом типа Subtree. Последующие изменения относились именно к этому временному объекту, а исходный объект не менялся.
Выводы:
  1. Передача объекта по константной ссылке в C++ может иметь двойственную семантику: во-первых это передача существующего объекта без возможности его изменения, во-вторых - создание нового временного объекта также без возможности его изменения. Иногда от программиста требуется значительное внимание, чтобы понять какой из вариантов сработает. В данном случае я ожидал, что будет работать первый вариант, но за счет неявного вызова конструктора Node сработал второй - был создан временный объект, изменение полей которого никак не влияло на исходный объект. Даже если этот недосмотр не отразится на правильном выполнении программы, он может незапланированно снизить ее производительность из-за затрат на создание временных объектов.
  2. Будьте внимательны с const_cast - его использование вероломно нарушает семантику передачи объекта по ссылке и может явиться причиной подобных проблем. Если бы я не использовал const_cast, компилятор просто не пропустил бы такой код.

3 комментария:

  1. ссылки вдойственны, но можно предугадать ход событий, существуют правила для константных ссылок:
    1. если константной ссылке присвоить объект того же типа что и ссылка(типы полностью совпадаю), то ссылка будет ссылаться на переменную, все изменения переменной отразятся на ссылке
    2. если константной ссылке присвоить объект другого типа, то будет создан временный объект и именно с ним будет связана ссылка, изменения же переменной не повлияют на ссылку, так как она ссылается на временный объект
    Например:

    int i = 6;
    unsigned ii = 66;

    const int &const_ref = i;
    const int &temp_ref = ii;

    ++i;
    ++ii;

    qDebug() << "const_ref = " << const_ref; // выведет 7
    qDebug() << "temp_ref = " << temp_ref; // выведет 66

    во втором случае была создана временная переменная

    ОтветитьУдалить
  2. Сергей, так и есть. Это именно то, о чем я хотел сказать.

    В данном случае аргументом BasicEval::operator()() является константная ссылка на объект типа Subtree, который является обычной структурой. Далее этот оператор передает данный объект методу BasicEval::GetScalarValue(), однако его типом является Node, который определен как

    typedef variant< Tree, Leaf > Node;

    в свою очередь Tree это рекурсивная обертка Subtree:

    typedef recursive_wrapper< Subtree > Tree;

    Поскольку boost::variant предназначен для прозрачного выбора типов, то передавать объект типа Subtree в метод GetScalarValue() можно без всяких дополнительных преобразований. Вот этот факт и ослабил мою бдительность. Несмотря на прозрачную передачу константной ссылки в метод GetScalarValue() на стеке последнего создается новый объект. И все изменения, произведенные с этим объектом внутри GetScalarValue() с помощью коварного const_cast остаются в этом объекте, и не влияют на состояние объекта Subtree, который был передан в BasicEval::operator().

    ОтветитьУдалить
  3. "Язык программирования C++. 3-е издание Б.Страуструп" русское издание, 138 страница, Глава 5.5

    ОтветитьУдалить