Форум » C/C++ » Типичная ошибка программистов и баг MS VC++ 2015 » Ответить

Типичная ошибка программистов и баг MS VC++ 2015

Сыроежка: Нередко программисты допускают следующую ошибку, которую визуально порой нелегко распознать. Допустим необходимо из вектора удалить все элементы, которые равны значению первого элемента вектора. Обычно для этих целей применяется связка вызовов стандартного алгоритма std::remove (или std::remove_if) совместе с вызовом метода erase класса std::vector. Вот как может выглядеть соответствующий код [pre2] #include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> v = { 1, 2, 3, 7, 1, 5, 4 }; v.erase( std::remove( v.begin(), v.end(), v[0] ), v.end() ); for ( int x : v ) std::cout << x << ' '; std::cout << std::endl; } [/pre2] Вы уже видите ошибку? Проблема в том, что алгоритм std::remove в качестве третьего параметра имеет параметр ссылочного типа. Вот как выглядет объявление алгоритма [pre2] template<class ForwardIterator, class T> ForwardIterator remove(ForwardIterator first, ForwardIterator last, const T& value); [/pre2] Параметр value имеет тип const T&. В приложении к показанной демонстрационной программе это означает, что ссылка на v[0] сразу же становится недействительной после удаления первого элемента вектора, потому что первый элемент вектора равен сам себе, то есть значению v[0]. В результате программа будет иметь неопределенное поведение. Действительно, если вы посмотрите вывод программы на консоль, то вы увидите следующее: [pre2] 2 3 7 1 5 4 [/pre2] Как видите только самый первый элемент вектора со значением равным 1 был удален. Другой элемент с этим же значением остался в векторе, так как область памяти, на которую ссылается параметр value уже больше не содержит 1, соответствующий элемент был удален. Как же правильно вызвать этот алгоритм, чтобы не вводить новое имя для промежуточной переменной, которая будет хранить значение, содержащееся в v[0]? Сделать это можно следующим образом: [pre2] #include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> v = { 1, 2, 3, 7, 1, 5, 4 }; v.erase( std::remove( v.begin(), v.end(), int{ v[0] } ), v.end() ); for ( int x : v ) std::cout << x << ' '; std::cout << std::endl; } [/pre2] Обратите внимание на фигурные скобки вокруг v[0]. Это так называемая функциональная нотация явного приведения типа с использованием списка инициализации. Такое приведение создает временный объект на основе значения v[0], а потому при удалении первого элемента вектора (то есть элемента с индексом 0) поведение программы будет корректным: удаляется элемент вектора, а не этот временный объект, ссылка на временный объект по-прежнему будет валидной. Вот соответствующая выдержка из стандарта C++ (5.2.3 Explicit type conversion (functional notation)): [quote]3 Similarly, a simple-type-specifier or typename-specifier followed by a braced-init-list creates a temporary object of the specified type direct-list-initialized (8.5.4) with the specified braced-init-list, and its value is that temporary object as a prvalue. [/quote] Теперь, если запустить на выполнение исправленную программу, то вывод уже будет правильным: [pre2] 2 3 7 5 4 [/pre2] Выражение int{ v[0] } нельзя заменить ни на int( v[0] ), ни на ( int )v[0], так как оба эти выражения возвращают lvalue v[0], а, следовательно, вы снова получите ссылку на v[0]. Это будет работать только, если вы выберете правильный компилятор! Как вы наверное уже сами догадывались, компилятор MS VC++ (Compiler version: 19.00.23015.0(x86) Last updated: Jun. 19, 2015) к таким не относится. Он все равно настойчиво возвращает ссылку на объект, даже если вы используете нотацию с фигурными скобками, а не создает временный объект. Поэтому для второй демонстрационной программы вы получите тот же самый результат, что имел место для первой демонстрационной программы. Можно убедиться в присутствии бага компилятора MS VC++ и более простым способом. Запустите следующую программу, и вы увидите, что переменная x будет изменена, что означает, что компилятор не создавал временного объекта: [pre2] #include <iostream> int main() { int x = 0; int{ x } = 10; std::cout << "x = " << x << std::endl; } [/pre2] Сообщение о наличии бага в компиляторе MS VC++ я уже послал в Майкрософт. Как же написать вызов алгоритма std::remove так, чтобы он правильно выполнялся любым компилятором? Сделать это можно очень просто! Достаточно вместо v[0] записать выражение такое, как, например, v[0] + 0: [pre2] v.erase( std::remove( v.begin(), v.end(), v[0] + 0 ), v.end() ); [/pre2] Выражение v[0] + 0 создает временный объект, к которому "привязывается" константная ссылка. Поэтому данный вызов алгоритма выдаст ожидаемый корректный результат. Тоже самое можно достичь также, просто поставив знак плюс перед v[0]: +v[0], То есть вы можете использовать любой прием, который превращает выражение с v[0] из lvalue в rvalue. Главное - чтобы ваши "танцы с бубном" вокруг v[0] были понятны читающему ваш код.

Ответов - 4

Сыроежка: Пришел ответ от Майкрософт. Оказывается это не баг компилятора, а расширение языка. Чтобы отключить это расширение языка нужно задать опцию компиляции /Zc:rvalueCast и тогда компилятор выдаст сообщение следующего содержания: error C2106: '=': left operand must be l-value Я всегда забываю, что у Майкрософт есть собственные расширения языка, которые установлены по умолчанию, и что их требуется отключать, чтобы получить код, соответствующий стандарту C++.

Сыроежка: Однако, рано ставить точку в этой теме. Сначала рассмотрим следующее задание, которое обычно задается начинающим программистам, хотя по-моему мнению это задание больше подходит для продвинутых программистов, так как не каждый продвинутый программист может сходу предложить правильное решение.. Итак, имеется четыре целых числа, хранящихся соответственно в переменных a, b, c и d, имеющих тип int. Используя четыре вызова стандартной функции [pre2] template<class T> pair<const T&, const T&> minmax(const T& a, const T& b); [/pre2] и не объявляя никаких дополнительных переменных (но можно создавать временные объекты), вывести на консоль минимальное и максимальное значения этих чисел. Хочу сразу же обратить внимание, что не разрешается заменить вышеуказанную функцию на ее перегруженный вариант [pre2] template<class T> pair<T, T> minmax(initializer_list<T> t); [/pre2] Использовать можно только заданную в условиях задания функцию. Свое решение (на самом деле два решения) я покажу в следующем сообщение, а пока у каждого есть время поразмыслить над этим заданием.

best_coder: >Выражение int{ v[0] } нельзя заменить ни на int( v[0] ) неправда. можно. >вы снова получите ссылку на v[0] ничего подобного. получим ссылку на временный объект.


Сыроежка: Хорошо, что вы на это обратили внимание. Это результат моей рассеянности, когда я писал сообщение. Дело в том, что изначально в примерах кода я вообще использовал выражение decltype( v[0] ), как, например, [pre2] v.erase( std::remove( v.begin(), v.end(), ( decltype( v[0] ) ) v[0] ), v.end() ); [/pre2] Затем для упрощения ввел typedef определение typedef decltype( v[0] ) T; и код выглядел следующим образом [pre2] typedef decltype( v[0] ) T; v.erase( std::remove( v.begin(), v.end(), T( v[0] ) ), v.end() ); [/pre2] или [pre2] typedef decltype( v[0] ) T; v.erase( std::remove( v.begin(), v.end(), ( T )v[0] ), v.end() ); [/pre2] А затем, когда уже писал сообщение,то решил упростить эти конструкции в примерах кода и записать просто [pre2] v.erase( std::remove( v.begin(), v.end(), ( int )v[0] ), v.end() ); [/pre2] При этом совершенно забыв, что ранее T представляло собой decltype( v[0[ ), что возвращало для ссылочного типа lvalue. Это и стало причиной моих неверных заключений, так как во всех примерах с использованием определенного мною имени T я получал lvalue Тем не менее сам пример типичной ошибки, нередко допускаемой программистами, и способ борьбы с ней посредством преобразования lvalue в rvalue, сохраняют свою актуальность.



полная версия страницы