One option for an incremental transition from std::vector
to pmr::vector
is to type-erase the vector
objects on the API, and instead use an object that is convertible to both std::vector
or pmr::vector
. If this conversion is implicit, then old code will continue to work without change as you alter the components to use pmr
You could simply use a conversion function everywhere -- but this can result in a lot of changes required for doing smaller incremental changes on each component. Hiding this behind the type makes it so that old code will behave as it used to while the transition occurs.
Short Version
A short outline of how to achieve this is to do the following
- Create conversion functions between
std::vector
and std::pmr::vector
, and vice-versa
- Create a wrapper type that:
- is implicitly constructible from both
std::vector
and std::pmr::vector
,
- is implicitly convertible to both
std::vector
and std::pmr::vector
, and
- uses the above conversion utilities implicitly to allow for conversions
- Convert the transitional APIs to use the wrapper type on any function arguments and return values, rather than the previous `std::vector
- Since this type is convertible to/from the different vector types, your existing code should continue to work -- while allowing you to migrate component-to-component
- Once all consumers no longer use
std::vector
, change the wrapped type back to std::pmr::vector
I'll go over this in more detail below.
Detailed Version
Note that no matter what process you take, there will always be some form of temporary overhead during the transitional period that will occur when converting between the two. This is because the allocator from std::vector
is not the same as a polymorphic allocator from pmr::vector
-- even if they both use new
/delete
under the hood. C++ provides no way to transition data between vectors using allocators of different types -- meaning the only way is to allocate a new block for the different vector, and either copy or move each object from the old vector.
I must emphasize that this cost is temporary, since it goes away once everything transitions over.
Conversion Functionality
You will still require conversion utilities as Mikael suggests in his answer; these will make the basis for an automatic converting object.
I've made a simple converter that just changes the vector
based on the Allocator
type. This does not take into account the new memory_resource
for the pmr type -- so you might want something more involved depending on your needs.
// Conversion functions for copying/moving between vectors
namespace detail {
// Conversion that copies all entries (const lvalue vector)
template <typename NewAllocator, typename T, typename OldAllocator>
std::vector<T, NewAllocator> convert_vector(const std::vector<T, OldAllocator>& v)
{
auto result = std::vector<T, NewAllocator>{};
result.reserve(v.size());
result.assign(v.begin(), v.end());
return result;
}
// conversion that moves all entries (rvalue vector)
template <typename NewAllocator, typename T, typename OldAllocator>
std::vector<T, NewAllocator> convert_vector(std::vector<T, OldAllocator>&& v)
{
auto result = std::vector<T, NewAllocator>{};
result.reserve(v.size());
result.assign(
std::make_move_iterator(v.begin()),
std::make_move_iterator(v.end())
);
return result;
}
} // namespace detail
Note: these conversion functions just change the allocator used in the vector, and have 2 overloads: one that copies each object, and one that will move each object. Since we can't move the underlying vector, this is the best we can do -- and will be a temporary overhead.
Wrapped Type
With this, we just need a simple type that we can use on APIs to normalize the vectors in some way. There are two key things we would want:
- If we make this type implicitly constructible from both
std::vector
and std::pmr::vector
, then we can use this type for arguments on the API -- since it can accept both.
- If we make the type implicitly convertible to both
std::vector
and std::pmr::vector
, then we can use this on return types from our component, since consumers can assign directly to it and it "just works".
So let's make this type:
// Type erased class that can behave as either vector
// Normalizes all vectors to a std::pmr::vector
template <typename T>
class AnyVector
{
public:
// Implicitly constructible from both std::vector and pmr::vector
// std::vector overloads need to convert to pmr::vector
AnyVector(const std::vector<T>& vec)
: m_storage{detail::convert_vector<std::pmr::polymorphic_allocator<T>>(vec)}
{}
AnyVector(std::vector<T>&& vec)
: m_storage{detail::convert_vector<std::pmr::polymorphic_allocator<T>>(std::move(vec))}
{}
AnyVector(const std::pmr::vector<T>& vec) // no cost
: m_storage{vec}
{}
AnyVector(std::pmr::vector<T>&& vec) // no cost
: m_storage{std::move(vec)}
{}
AnyVector(const AnyVector&) = default;
AnyVector(AnyVector&&) = default;
// AnyVector& operator= for vector objects is less important, since this is meant
// to exist on the API boundaries -- but could be implemented if there's a need.
// Implicitly convertible to std::vector
operator std::vector<T>() const
{
return detail::convert_vector<std::allocator<T>>(current);
}
operator std::vector<T>() &&
{
return detail::convert_vector<std::allocator<T>>(std::move(current));
}
// Implicitly convertible to std::pmr::vector
operator std::pmr::vector<T>() const
{
return m_storage;
}
operator std::pmr::vector<T>() &&
{
return std::move(m_storage);
}
private:
std::pmr::vector<T> m_storage;
};
This is simple enough: It's a type that can be implicitly constructed from both std::vector
and std::pmr::vector
, and it can be converted to both as well. Internally it stays normalized on std::pmr::vector
, since this is the end-goal.
Putting it all together
Now you can use this on your APIs that you want to support transitioning to. Using the code from your question:
class ComponentObjects
{
public:
...
void Update(AnyVector<OtherClass> par1,
const OtherClassTwo& par2,
const double par4,
const OtherClassThree& par5,
OtherClassFour<>* par6,
uint64_t par7,
const OtherClassFive& par8,
const OtherClassSix& par9);
AnyVector<OtherClassSeven> DoSomething() const { return priv_mem_one; }
AnyVector<OtherClassEight> DoSomethingElse() const { return priv_mem_two; }
private:
std::pmr::vector<ClassA> priv_mem_one{};
std::pmr::vector<ClassA> priv_mem_two{};
const ObjectParameter par_one_{};
const ObjectParameter par_two_{};
};
Things to note here:
Update
now accepts an AnyVector
, so that internally you may convert this to a std::pmr::vector<OtherClass>
.
- This is accepted by-value rather than
const
reference, so that in your consuming code you can std::move
this object to a std::pmr::vector
which will be a true move without a conversion (lightweight)
- Consumers can still call this code with the old
std::vector
or the new std::pmr::vector
.
- Once all consumers are migrated to
std::pmr::vector
, you can remove AnyVector
and replace it with std::pmr::vector
priv_mem_one
and priv_mem_two
are now std::pmr::vector
s -- since this is the desired internal structure
DoSomething()
and DoSomethingElse
now return AnyVector
objects by value.
- References are cheaper, but if this type is needed by both
std::vector
and std::pmr::vector
consumers, then this will guarantee that both can consume this. This will be necessary even if you chose to convert everywhere manually -- since a std::vector
would eventually be needed somewhere.
- Because
DoSomething
and DoSomethingElse
return AnyVector
, all consumers can continue to use this with either std::vector
or std::pmr::vector
.
- If a caller is trying to consume this such as a
std::vector
, this will trigger a move conversion because the type being returned is by-value (which is a PR-value, and triggers the &&
overload of conversion).
- If a caller is trying to consume this as a
std::pmr::vector
, the consumer will see a move of the vector itself -- which is lightweight.
- As with above, once all consumers migrate to
std::pmr::vector
, these types can be changed back to no longer be AnyVector
std::vector
can't be changed, and will need some conversions even when this effort is complete? – aschepler