This is quite easy as long as you don't require ownership, as you mention. The solution is to use a type-erased contiguous sequence. If at some point you do desire ownership, it gets a bit more complicated. I've added information on this to the bottom of this answer.
Non-owning
If you are using c++20, you can make use of std::span -- which is a non-owning wrapper around contiguous data such as std::vector, std::array, etc.
If you are using anything prior to c++20, you can always use or base an implementation off of a pre-canned span implementation, such as gsl::span from the Guideline Support Library, or bpstd::span from BackportC++ which is c++11 compatible.
Or alternatively find any array_view-like type which accomplishes the same thing. There are many existing open-source solutions that can be leveraged for this purpose.
Owning
If you do require ownership, then more work will be involved -- especially if you want the data to come from different contiguous sequence types.
You will require owning type-erasure for such a purpose, and at some point a vector will either need to be moved or copied -- one of the two. If you can allow the clients the option to move their vectors, this would be much cheaper than performing a deep copy of the data.
To do this, you will need to have an interface, and a class-template implementation type, all wrapped into a nice "vector"-like API. The captured data can then be held in the class-template implementation indirectly, behind a unique_ptr:
template <typename T>
class AnyVector
{
public:
template <typename Container>
explicit AnyVector(Container&& container)
: m_container{std::make_unique<Concrete<std::decay_t<Container>>(std::forward<Container>(container))}
{
}
const T& operator[](std::size_t index) const
{
return m_container->get(index);
}
const T& at(std::size_t index) const
{
if (index >= m_container->size()) {
throw std::out_of_range{"AnyVector<T>::at"};
}
return m_container->get(index);
}
std::size_t size() const
{
return m_container->size();
}
private:
// The interface we want all types to follow
class Interface
{
public:
// Make this API as deep as you need it to be
virtual ~Interface() = default;
virtual const T& get(std::size_t index) const = 0;
virtual std::size_t size() const = 0;
};
// The concrete version of the interface, in terms of the underlying container
template <typename Underlying>
class Concrete : public Interface
{
public:
template <typename TUnderlying>
Concrete(TUnderlying&& underlying)
: m_underlying{std::forward<TUnderlying>(underlying)}
{
}
virtual const T& get(std::size_t index) const override { return m_underlying[index]; }
virtual std::size_t size() const override { return m_underlying.size(); }
private:
Underlying m_underlying;
}
std::unique_ptr<Interface> m_container;
};
The above code only works if the input container defines a T::size() and T::operator[] function. If the underlying vectors you want to own have functions that are differently named, you will need to be more creative in solving this -- either through having the user explicitly specify with a traits-like type, or alternatively using non-member functions with ADL.
In the latter case, this could be achieved by doing something like:
std::size_t size() const override
{
// ADL-find 'size'
return size(m_container);
}