2
votes

I'm currently updating a component to use pmr::vector containers, rather than std::vector. Unfortunately, the component is complex and there is a good deal of class hierarchies and dependencies outside of the component. Furthermore, std::vector is part of many of these interfaces.

Because std::vector and pmr::vector are incompatible, I'm having difficulty isolating any updates I make in the component. As the component is somewhat large, I'd like to make incremental updates, but I can't wrap my head around a good method for doing so and it's not for a lack of effort.

Typically, I would use an adapter class and override the function calls to the base class, as shown below.

class OldClass {
 public:
  virtual ~OldClass() = default;

  virtual std::vector DoSomething() const {
    return some std::vector;
  }
};

class NewClass {
 public:
  pmr::vector DoSomething() const {
    return some pmr::vector;
  }
};

class Adapter : public OldClass {
 private:
  NewClass *adaptee_;

 public:
  Adapter(NewClass *adaptee) : adaptee_(adaptee) {}
  pmr::vec DoSomething() const override {
  }
};

However, I'm dealing with a problem cutting out a clear use case for this type of implementation. An example of a case I'm seeing would be something like below.

class ComponentObjects
{
  public:
    struct ObjectParameters
    {
        size_t number_of_steps;
        double time;
    };
    ComponentObjects(ObjectParameters one, ObjectParameters two);

    void Update(const std::vector<OtherClass>& par1,
                const OtherClassTwo& par2,
                const double par4,
                const OtherClassThree& par5,
                OtherClassFour<>* par6,
                uint64_t par7,
                const OtherClassFive& par8,
                const OtherClassSix& par9);

    const std::vector<OtherClassSeven>& DoSomething() const { return priv_mem_one; }

    const std::vector<OtherClassEight>& DoSomethingElse() const { return priv_mem_two; }

  private:
    std::vector<ClassA> priv_mem_one{};
    std::vector<ClassA> priv_mem_two{};
    const ObjectParameter par_one_{};
    const ObjectParameter par_two_{};
};

Thank you in advance for any help.

2
Why isolate? Why not globally replace?Mooing Duck
So some external interfaces involving std::vector can't be changed, and will need some conversions even when this effort is complete?aschepler
@MooingDuck this would be the preferred method, but unfortunately I'm only able to change a single component. It's a large project.dsell002
@aschepler That is correct.dsell002
Just to check if I understood your question correctly : 1. You want to replace std::vector with pmr::vector as much as possible 2. There are places where you will be forced to keep std::vector So you want a way to replace as easily as possible std with pmr without requiring everything to migrate to pmr?Mickaël C. Guimarães

2 Answers

3
votes

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::vectors -- 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
3
votes

The only solution I see for you is having Convert functions between pmr::vector and std::vector.

This would make easy to only use pmr::vector at specific spots. For example, a half-converted Update function as you mentioned would look like :

void ComponentObjects::Update(const std::vector<OtherClass>& par1,
                const OtherClassTwo& par2,
                const double par4,
                const OtherClassThree& par5,
                OtherClassFour<>* par6,
                uint64_t par7,
                const OtherClassFive& par8,
                const OtherClassSix& par9)
{
  const pmr::vector<OtherClass> pmrPar1 = ToPmr(par1).

  // Implement the rest using pmr vector
  ...  
}

Of course, this has drawback of performance penalty : you will introduce data conversions. It might be a problem and is less than ideal, but depending on the data stored on those vectors it might be an irrelevant issue.

Your convert functions would look like :

template <class T>
pmr::vector<T> ToPmr(const std::vector<T>& input)
{
  pmr::vector<T> output;
  output.reserve(input.size());
  std::copy(input.begin(), input.end(), std::back_inserter(output.begin()));
  return output;
}

and

template <class T>
std::vector<T> ToStd(const pmr::vector<T>& input)
{
  std::vector<T> output;
  output.reserve(input.size());
  std::copy(input.begin(), input.end(), std::back_inserter(output.begin()));
  return output;
}

You can replace std::copy with whatever more optimized copying between vectors or use move semantics (contribution from Human-Compiler). I don't feel confident enough to provide a solution using move semantics for this problem, maybe Human-Compiler will provide an additional answer detailing how would an implementation look like with them...

If you have access to pmr and are willing to change it, it would be a good idea to integrate those conversion utilities into it. You could, for example, have a pmr constructor that takes the std::vector as a parameter and could take advantage of knowing the internals of pmr to do a more optimized copy.