Нередко программисты допускают следующую ошибку, которую визуально порой нелегко распознать.
Допустим необходимо из вектора удалить все элементы, которые равны значению первого элемента вектора. Обычно для этих целей применяется связка вызовов стандартного алгоритма
std::remove (или
std::remove_if) совместе с вызовом метода
erase класса
std::vector.
Вот как может выглядеть соответствующий код
#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;
}
Вы уже видите ошибку?
Проблема в том, что алгоритм
std::remove в качестве третьего параметра имеет параметр ссылочного типа. Вот как выглядет объявление алгоритма
template<class ForwardIterator, class T>
ForwardIterator remove(ForwardIterator first, ForwardIterator last, const T& value);
Параметр
value имеет тип
const T&. В приложении к показанной демонстрационной программе это означает, что ссылка на
v[0] сразу же становится недействительной после удаления первого элемента вектора, потому что первый элемент вектора равен сам себе, то есть значению
v[0].
В результате программа будет иметь неопределенное поведение. Действительно, если вы посмотрите вывод программы на консоль, то вы увидите следующее:
2 3 7 1 5 4
Как видите только самый первый элемент вектора со значением равным 1 был удален. Другой элемент с этим же значением остался в векторе, так как область памяти, на которую ссылается параметр
value уже больше не содержит 1, соответствующий элемент был удален.
Как же правильно вызвать этот алгоритм, чтобы не вводить новое имя для промежуточной переменной, которая будет хранить значение, содержащееся в
v[0]?
Сделать это можно следующим образом:
#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;
}
Обратите внимание на фигурные скобки вокруг
v[0]. Это так называемая функциональная нотация явного приведения типа с использованием списка инициализации. Такое приведение создает временный объект на основе значения
v[0], а потому при удалении первого элемента вектора (то есть элемента с индексом 0) поведение программы будет корректным: удаляется элемент вектора, а не этот временный объект, ссылка на временный объект по-прежнему будет валидной.
Вот соответствующая выдержка из стандарта
C++ (5.2.3 Explicit type conversion (functional notation)):
цитата: |
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. |
|
Теперь, если запустить на выполнение исправленную программу, то вывод уже будет правильным:
2 3 7 5 4
Выражение
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 будет изменена, что означает, что компилятор не создавал временного объекта:
#include <iostream>
int main()
{
int x = 0;
int{ x } = 10;
std::cout << "x = " << x << std::endl;
}
Сообщение о наличии бага в компиляторе
MS VC++ я уже послал в Майкрософт.
Как же написать вызов алгоритма
std::remove так, чтобы он правильно выполнялся любым компилятором?
Сделать это можно очень просто! Достаточно вместо
v[0] записать выражение такое, как, например,
v[0] + 0:
v.erase( std::remove( v.begin(), v.end(), v[0] + 0 ), v.end() );
Выражение
v[0] + 0 создает временный объект, к которому "привязывается" константная ссылка. Поэтому данный вызов алгоритма выдаст ожидаемый корректный результат. Тоже самое можно достичь также, просто поставив знак плюс перед
v[0]:
+v[0], То есть вы можете использовать любой прием, который превращает выражение с
v[0] из
lvalue в
rvalue.
Главное - чтобы ваши "танцы с бубном" вокруг
v[0] были понятны читающему ваш код.