Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to convert std::vector<const T*> to std::vector<T*> without extra allocations?

Let's say I have a Storage of some Objects which has a method that aggregates pointers to some of the Objects in a vector. Like this:

class Storage
{
public:
   std::vector<Object*> aggregate_some_objects(); // non-const version
   std::vector<const Object*> aggregate_some_objects() const; // const version

private:
   std::unordered_map<size_t, Object> m_objects; // data is stored 
                                                 // by-value in a non-vector container
}

Generally, there is way to avoid copy-paste in implementation of const + non-const method pairs by calling one of them inside the other with the help of const_cast. Here however this is not possible because the return types of the methods are different.

The most straightforward way to avoid copy-paste here would be to call const version from a non-const version and use the returned std::vector<const T*> to populate a separate std::vector<T*>. However this would lead to at least 2 heap allocation (one for each vector). I would like to avoid the allocations associated with the second vector.

I wonder if there is way to write something like

template <typename T>
std::vector<T*> remove_const_from_vector_of_ptrs(std::vector<const T*>&& input)
{
   std::vector<T*> ret;
   // do some magic stuff here that does not involve
   // more memory allocations
   return ret;
}

Thus, allowing to write

std::vector<const Object*> Storage::aggregate_some_objects() const
{
   // non-trivial implementation
}

std::vector<Object*> Storage::aggregate_some_objects() 
{
   auto objects = const_cast<const Storage*>(this)->aggregate_some_objects();
   return remove_const_from_vector_of_ptrs(std::move(objects));
}

There is no 'release' method in std::vector (like std::unique_ptr for example) that allows transferring of memory ownership - and for a very good reason, so I expect that this is not possible.

I also understand that if it were possible, it would be a dangerous operation that should be generally avoided, just as const_cast. But a careful usage in cases like this seems more beneficial than copy-pasting.

Edit: added clarifications to what do I mean by 'extra' allocations and changed Storage::aggregate_objects() to Storage::aggregate_some_objects() to better indicate that the implementation of these methods is more complex then a range-based loop - hence the desire to avoid copy-pasting the implementation.

like image 384
Sergey Nikitin Avatar asked Jun 11 '16 13:06

Sergey Nikitin


1 Answers

The short answer is: no. std::vector<Object*> and std::vector<const Object*> are two different, independent classes. They are as different from each other as class A is from class B. It is often thought that just because both of them start with std::vector, that they are somehow related to each other. This is not true, and for that reason there is no way to convert one to the other, "in place". Each one of these vector classes own their corresponding internal data(), and will not willingly give it up to some other, strange class.

The long answer is still no, but in many cases it's possible to work around this in order to avoid manual code duplication. The truth of it is that code duplication is inevitable in most of these kinds of situations, and the best that can be done is to avoid manual code duplication.

One common approach is have both the constant and the mutable class method be facades for a single, shared, private template:

// Header file:

class Storage {

public:

    std::vector<const Object*> aggregate_objects() const;

    std::vector<Object*> aggregate_objects();

private:

    template<typename v_type> void make_aggregate_objects(v_type &v) const;
};

// In the translation unit:

template<typename v_type> void Storage::make_aggregate_objects(v_type &v) const
{
     // Now, create 'v' here... v.reserve(), v.push_back(), etc...
}

std::vector<const Object*> Storage::aggregate_objects() const
{
     std::vector<const Object *> v;

     make_aggregate_objects(v);

     return v;
}

std::vector<Object*> Storage::aggregate_objects()
{
     std::vector<const Object *> v;

     make_aggregate_objects(v);

     return v;
}

The compiler will still generate two nearly identical chunks of code, but at least it's not you doing all the typing.

Another, similar approach, is to pass a lambda to the template function instead of passing a vector object, with the private template function using the lambda function as a callback, to construct the returned vector. With a bit of type erasure, and some help from std::function, the private class method can be turned into an ordinary method, instead of a template method.

like image 55
Sam Varshavchik Avatar answered Sep 28 '22 05:09

Sam Varshavchik