Moving between two different contiguous containers

269 Views Asked by At

I have a std::vector<double> that I have to move to a boost::container::flat_set<double>. Both containers are contiguous, so after sorting the vector in principle I could move the data from one to the other.

Is there a way to move the whole data between these two different containers?

Please, take into account that I want to move the whole data, not element by element.

I can move data between containers of the same type, but not between different containers.

std::vector<double>  v1 = ...
std::sort(v1.begin(), v1.end());

std::vector<double>  v2(std::move(v1)); // ok
boost::flat_set<double> f2(v1.begin(), v1.end()); // doesn't move, it copies
boost::flat_set<double> f3(std::move(v1)); // doesn't compile

It seems that for this to work flat_set should have a move constructor from containers with .data(), where the pointer is stolen from the argument.

2

There are 2 best solutions below

1
R2RT On

I believe there is some way to verify whenever data alignment in both containers match and memcpy could be used (and source cleared without destructing) exists and maybe someone will share it with us, but as long as we want to use STL there is a way: the std::move_iterator. It makes your container constructor move elements instead of copying. It does not remove elements out of source container though, but leaves them stateless (e.g. empty strings as in example).

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <boost/container/flat_set.hpp>

int main()
{   
    std::vector<std::string>  v1 = {"a","v","d"};
    std::sort(v1.begin(), v1.end());

    std::vector<std::string>  v2(std::move(v1)); // ok
    boost::container::flat_set<std::string> f1(std::make_move_iterator(v2.begin()), std::make_move_iterator(v2.end())); // moves, but does not remove elements from of source container


    for(auto& s : v1)
        std::cout << "'" << s << "'" << ' ';
    std::cout << " <- v1 \n";

    for(auto& s : v2)
        std::cout << "'" << s << "'" << ' ';
    std::cout << " <- v2 \n";

    for(auto& s : f1)
        std::cout << "'" << s << "'" << ' ';
    std::cout << " <- f1 \n";
}

Output

 <- v1 
'' '' ''  <- v2 
'a' 'd' 'v'  <- f1 

Online code: https://wandbox.org/permlink/ZLbocXKdqYHT0zYi

7
alfC On

It looks like it is not possible without modifying the constructor boost::container::flat. Without modifying either class it seems that the only a hack would do it, for example using reinterpret_cast. The solution I found is either to use an alternative implementation of vector or very ugly code.

Before going into my solution, I must that say that this is probably a defect of both classes. These clases should have a set of release()/aquire(start, end) functions that respectively returns the pointer range to the data releasing the ownership and gets the pointer range owning it from then on. An alternative could be to have a constructor that moves from any other container that has a the data member function.

Solution using reinterpret_cast and a different implementation of vector

It turns out that reinterpret_casting from std::vector to boost::container::flat_set is not possible, because the layout is not compatible. However it is possible to reinterpret_cast from boost::container::vector to boost::container::flat_set out of the box (that is because they have a common implementation).

#include<cassert>
#include<boost/container/flat_set.hpp>

int main(){
    boost::container::vector<double> v = {1.,2.,3.};
    boost::container::flat_set<double> fs = std::move(reinterpret_cast<boost::container::flat_set<double>&>(v));

    assert(v.size() == 0);
    assert(*fs.find(2.) == 2.);s
    assert(fs.find(4.) == fs.end());
}

So, I can replace std::vector by boost::container::vector and I can move data to a flat_set.

Non-portable solution using std::vector and ugly code

The reason the layout of std::vector and boost::container::vector are different is that boost::container::vector stores metadata in this way:

class boost::container::vector{
   pointer     m_start;
   size_type   m_size;
   size_type   m_capacity;
}

while std::vector (in GCC) is basically pure pointers,

class std::vector{
    pointer _M_start;
    pointer _M_finish;
    pointer _M_end_of_storage;
}

So, my conclusion is that moving is possible only through a hack given that the implementation I use of std::vector is not compatible with boost::container::flat_set.

In an extreme case, one can do this (sorry if this code offends someone, the code is not portable):

template<class T>
boost::container::flat_set<T> to_flat_set(std::vector<T>&& from){
//  struct dummy_vector{T* start; T* finish; T* end_storarge;}& 
//      dfrom = reinterpret_cast<dummy_vector&>(from);
    boost::container::flat_set<T> ret;
    struct dummy_flat_set{T* start; std::size_t size; std::size_t capacity;}& 
        dret = reinterpret_cast<dummy_flat_set&>(ret); 
    dret = {from.data(), from.size(), from.capacity()};
//  dfrom.start = dfrom.finish = dfrom.end_storarge = nullptr;
    new (&from) std::vector<T>();
    return ret;
};

int main(){
    std::vector<double> v = {1.,2.,3.};
    boost::container::flat_set<double> fs = to_flat_set(std::move(v));

    assert(v.size() == 0);
    assert(*fs.find(2.) == 2.);
    assert(fs.find(4.) == fs.end());
}

Note that I am not taking into account allocator issues at all. I am not sure how to handle allocators here.

In retrospect I don't mind using a form of cast for this specific problem, because somehow I have to tell that the vector is sorted before moving to flat_set. (The problem is that this goes to extreme because it is a reinterpret_cast.) However this is a secondary issue, there should be legal way to move from std::vector to boost::container::vector.