Moshe Rabaev Moshe Rabaev - 3 months ago 15
C++ Question

Behavior with std::erase() non-references vs reference

Suppose I have a vector of type

my_object
which has a size of 3 and I want to get 3 elements from my vector storing them in a reference

Then I want to remove and erase element_3 by using
std::remove_if()
and element_1 and element_2 by using
std::remove


Here is
my_object
:

class my_object {
public:
my_object(int num);
bool exists() const;
private:
int num;
};
my_object::my_object(int num) : num(num) {}
bool my_object::exists() { return num == 1; }


Here is
main
:

std::vector<my_object> my_vector;
int main() {
my_object e1(2);
my_object e2(2);
my_object e3(1); // i.e exists() will return true in lambda

my_vector.push_back(e1);
my_vector.push_back(e2);
my_vector.push_back(e3);

const auto& element_1 = my_vector.at(0);
const auto& element_2 = my_vector.at(1);
const auto& element_3 = my_vector.at(2);

auto lambda = [](auto& src) { return src.exists() };
std::erase(std::remove_if(b, e, lambda), e); // remove_if for element_3
std::erase(std::remove(b, e, element_1), e);
std::erase(std::remove(b, e, element_2), e);
return 0;
}


What is extremely weird is that when I declare element_1, element_2, element_3 by reference than the erasing isn't done properly and the size isn't decreased to 0, but when I write
const auto
with no
&
then it works perfectly fine, can anyone explain this weird behavior to me?

Answer

Discounting the methods of erasure, those references are just that: references to objects living in the container. Once remove or remove_if have performed their tasked move-assignment while marching up the sequence, those references are still referring to the same elements, but the occupants of those slots are:

  • At best, still valid objects because a valid object either stayed where it was, or one was move-assigned there.
  • Mere shells of some former self because the reference now refers to a source object that was never reclaimed by a targeted move.

I'm not going to dive into std::remove, Rather. Look at this rather trivial example of std::remove_if

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> v = { 1,2,3,4,5 };
    const auto& a1 = v.at(0);
    const auto& a2 = v.at(2);
    const auto& a3 = v.at(4);

    std::cout << a1 << ' ' << a2 << ' ' << a3 << '\n';

    std::remove_if(v.begin(), v.end(), [](const auto& x) { return x == 3; });

    std::cout << a1 << ' ' << a2 << ' ' << a3 << '\n';
}

Output

1 3 5
1 4 5

As you can see, the functional description of std::remove_if lives up to what you see in the code. The 3 element was removed, and the 4 element was move-assigned to its place. What you don't see here is that the 5 element was move-assigned to the 4's place, and the 5 value you see here now happens to be coming from the slot where 5 was. The standard says that object is "valid", but with an "unspecified" value. We can verify that by ensuring our move-source of a move-assignment is, in fact, "invalid" (as far as we're concerned). Modifying our original program gives us this:

#include <iostream>
#include <vector>
#include <algorithm>

struct S
{
    S(int n) : value(n), isvalid(true)
    {
    }

    S(const S& s) : value(s.value), isvalid(true)
    {
    }

    S(S&& s) : value(s.value), isvalid(true)
    {
        s.isvalid = false;
    }

    S& operator =(S&& s)
    {
        value = s.value;
        isvalid = s.isvalid;

        s.isvalid = false;
        return *this;
    }

    int value;
    bool isvalid;
};

std::ostream& operator <<(std::ostream& outp, const S& s)
{
    outp << s.value << '(' << std::boolalpha << s.isvalid << ')';
    return outp;
}

int main()
{
    std::vector<S> v = { 1,2,3,4,5 };
    const auto& a1 = v.at(0);
    const auto& a2 = v.at(2);
    const auto& a3 = v.at(4);

    std::cout << a1 << ' ' << a2 << ' ' << a3 << '\n';

    std::remove_if(v.begin(), v.end(), [](const auto& x) { return x.value == 3; });

    std::cout << a1 << ' ' << a2 << ' ' << a3 << '\n';
}

Output

1(true) 3(true) 5(true)
1(true) 4(true) 5(false)

The bottom line: your references are still referring to the same slots they were before, but the elements have been either (a) move assigned to something else, or (a) no longer containing specified content. Tread carefully when using references to container contents when performing container modifications.

I reserve comment on the std::erase calls, as I have no idea what you're doing there at all. To my knowledge that isn't even a function in the standard library (wouldn't be the first time I missed out on a new function, but scraping over cppreference yields nothing, so take that for what its worth).

Comments