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. Последующие изменения относились именно к этому временному объекту, а исходный объект не менялся.
Выводы:
- Передача объекта по константной ссылке в C++ может иметь двойственную семантику: во-первых это передача существующего объекта без возможности его изменения, во-вторых - создание нового временного объекта также без возможности его изменения. Иногда от программиста требуется значительное внимание, чтобы понять какой из вариантов сработает. В данном случае я ожидал, что будет работать первый вариант, но за счет неявного вызова конструктора Node сработал второй - был создан временный объект, изменение полей которого никак не влияло на исходный объект. Даже если этот недосмотр не отразится на правильном выполнении программы, он может незапланированно снизить ее производительность из-за затрат на создание временных объектов.
- Будьте внимательны с const_cast - его использование вероломно нарушает семантику передачи объекта по ссылке и может явиться причиной подобных проблем. Если бы я не использовал const_cast, компилятор просто не пропустил бы такой код.
ссылки вдойственны, но можно предугадать ход событий, существуют правила для константных ссылок:
ОтветитьУдалить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
во втором случае была создана временная переменная
Сергей, так и есть. Это именно то, о чем я хотел сказать.
ОтветитьУдалитьВ данном случае аргументом 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().
"Язык программирования C++. 3-е издание Б.Страуструп" русское издание, 138 страница, Глава 5.5
ОтветитьУдалить