Blasco
Blasco

Reputation: 1755

Modify the content of a std container through a different container

I don't know if this is the right approach, but I think it explains what I'm trying to achieve.

I have three vectors:

std::vector<int> v1 = {1,2,3};
std::vector<int> v2 = {5,6,7};
std::vector<int> v3 = {8,9,10};

I would like to create a vector that contains references to the first elements of these vectors, I've tried doing it as follows:

std::vector<std::reference_wrapper<int>> v;
v.push_back(v1[0]);
v.push_back(v2[0]);
v.push_back(v3[0]);

so I could do:

std::rotate(v.begin(),v.begin+1,v.end())

and get:

v1 = 5, 2, 3
v2 = 8, 6, 7
v3 = 1, 9, 10

it almost works, doing the following modifies the original vectors:

++v[0];

But assignment doesn't work:

v[0] = new_value; // doesn't compile

Nor std::rotate has any affect.

How could I make this work?

Code

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

void print_vector(std::vector<int> &v) {
    std::for_each(v.begin(),v.end(),[](auto& x){std::cout << x << " ";});
    std::cout << "\n";
}

int main() {

    std::vector<int> v1 = {1,2,3};
    std::vector<int> v2 = {5,6,7};
    std::vector<int> v3 = {8,9,10};

    std::vector<std::reference_wrapper<int>> v;

    v.push_back(v1[0]);
    v.push_back(v2[0]);
    v.push_back(v3[0]);

    // This doesn't work, it rotates the references but not the values
    std::rotate(v.begin(),v.begin()+1,v.end());
    print_vector(v1);
    print_vector(v2);
    print_vector(v3);

    // Never the less this does work
    ++v[0];
    print_vector(v1);
    print_vector(v2);
    print_vector(v3);

    //v[0] = 3; // Assigment doesn't compile


    return 0;
}

Upvotes: 2

Views: 428

Answers (2)

F. Renato
F. Renato

Reputation: 21

Rotating the references into v doesn't rotate the values of the three vectors. So the solution I came up with is to make a copy of the rotated values and assign them back to the value referenced from v.

To perform the rotation:

std::vector<int> v_rotated;

std::rotate_copy(v.begin(), v.begin() + 1, v.end(),
    std::back_inserter(v_rotated));

The vector v_rotated contains the values with the order I want. Now I need to assign them to the values referenced in v. Using algorithms such as std::copy is not a viable solution because that would assign the references instead of values. In fact, as already stated from the other answer,

The assignment operator of std::reference_wrapper (std::reference_wrapper::operator=) does not assign a new value to the referenced element, it rebinds the wrapper

But I can take advantage of the implicit conversion operator 1 and using std::transform2 to carry out the copy:

    auto copy = [](int &from, int &to) {
    to = from;
    return std::ref(to);
};

This lambda will be used as a binary operation in std::transform. When an std::reference_warpper<int> is passed as one of the arguments, it returns the stored reference.

std::transform(v_rotated.begin(), v_rotated.end(),
    v.begin(), v.begin(),
    copy);

Since std::transform needs a destination container I choose to use v as a destination container and copy returns the reference to having the vector unchanged. That would be equivalent to:

auto it_rotated = v_rotated.begin();
for (auto it = v.begin(); it < v.end(); it++, it_rotated++)
{
    int &from = *it_rotated;
    int &to = *it;
    to = from;
}

Or using the explicit conversion

for(size_t i = 0; i < v.size(); i++)
{
    v[i].get() = v_rotated[i];
}

Upvotes: 1

Holt
Holt

Reputation: 37706

The assignment operator of std::reference_wrapper (std::reference_wrapper::operator=) does not assign a new value to the referenced element, it rebinds the wrapper. So basically:

std::vector<std::reference_wrapper<int>> v;
int a = 0;
v[0] = a;

assert( &v[0].get() == &a ); // true

If you want to assign a new value to the referenced element, you need to be explicit:

v[0].get() = a;

If you want v[0] = a; to work as you expect, or even std::rotate (because it actually swaps the reference, not the value), you may write your own wrapper:

/**
 * Class implementing std::reference_wrapper that
 * cannot be rebound after creation.
 *
 **/
template <class T>
class single_bind_reference_wrapper {

    // pointer to the original element
    T *p_;

public: // typedefs

    using type = T;

    // construct/copy/destroy
    single_bind_reference_wrapper(T& ref) noexcept : p_(std::addressof(ref)) {}
    single_bind_reference_wrapper(T&&) = delete;

    // Enable implicit convertsion from ref<T> to ref<const T>,
    // or ref<Derived> to ref<Base>
    template <class U, std::enable_if_t<std::is_convertible<U*, T*>{}, int> = 0>
    single_bind_reference_wrapper(const single_bind_reference_wrapper<U>& other) noexcept :
        p_(&other.get()) { }

    // assignment
    template <class U>
    decltype(auto) operator=(U &&u) const 
          noexcept(std::is_nothrow_assignable<T, U>{}) {
        return get() = std::forward<U>(u);
    }

    decltype(auto) operator=(const single_bind_reference_wrapper& other) const
          noexcept(std::is_nothrow_assignable<T, T>{}) {
        return get() = other.get();
    }


    // access
    operator T& () const noexcept { return *p_; }
    T& get() const noexcept { return *p_; }
};

You will need to provide a custom swap functions for most algorithm to work properly, something like:

template <class T>
void swap(single_bind_reference_wrapper<T> &lhs,
          single_bind_reference_wrapper<T> &rhs)
    noexcept(std::is_nothrow_move_constructible<T>::value &&
             std::is_nothrow_move_assignable<T>::value){
    auto tmp = std::move(lhs.get());
    lhs = std::move(rhs.get());
    rhs = std::move(tmp);
}

Upvotes: 4

Related Questions