简体   繁体   中英

C++: elegant “polymorphism” with STL types and custom types?

Let's say I have a custom implementation of std::vector named my_vector , and they have the same interface.

There are many functions that take std::vector (or its pointer/reference) as input. I want to make as little modification as possible to these functions, while allowing them to accept either std::vector or my_vector .

Replacing these functions with function templates is a way to go, but there seem to be a lot of change (and hence difficult to rollback), and they can't do type checking unless I use std::is_same to further complicate them. Function overloading is another alternative, but it entails duplicating the code, and is also difficult to rollback the change.

Question : can I design a wrapper type Vector for std::vector and my_vector , and let those functions accept Vector (or its pointer/reference) instead? But the problem with polymorphism is std::vector can't inherit a custom class.

Ideally, the code should similar to this (not required to be the same) :

Definition of function:

// void foo(std::vector<char> vec) {...} // old
void foo(Vector<char> vec) {...}  // new

Use the function:

std::vector<char> stdvec(5,'a');
my_vector<char> myvec(5,'a');
...

foo(stdvec);
foo(myvec);

Thanks!

PS1: accepting iterators instead of objects of containers is a good advice. However, there are some operations that cannot be achieved by iterators (like, push_back() , resize() , reserve() ):

void extendVectorAndAddGarbage(std::vector<char> &vec) {
    size_t so=vec.size();
    vec.reserve(sz*100);
    for (size_t i=sz; sz<sz*100;++sz) {...}
}

PS2: the container might not be std::vector - I am asking about using "polymorphism" on a STL type and a custom type with the same interface.

PS3: don't hesitate to write answers if you think it is not achievable.

Question: can I design a wrapper type Vector for std::vector and my_vector, and let those functions accept Vector (or its pointer/reference) instead? But the problem with polymorphism is std::vector can't inherit a custom class.

I am really a fan of OO with inheritance and polymorphism and all that cool stuff, but even more I am a fan of not doing OO when I dont need to. Yes of course you can write a wrapper that lets you use whatever types polymorphically and you can use inheritance (even multiple if you want), but why would you? Do you really want to write all that boilerplate?

If you want minimum changes on existing code, like in

// void foo(std::vector<char> vec) {...} // old
void foo(Vector<char> vec) {...}  // new

then make it a template:

template <typename T> void foo(T vec) {...}

For your example, that would be

template <typename T> 
void foo(T &vec) {
    size_t so=vec.size();
    vec.reserve(sz*100);
    for (size_t i=sz; sz<sz*100;++sz) {...}
}

If you really want to prevent yourself from instantiating the template with something different than std::vector or my_vector then you could do for example this:

namespace {
    template <typename T> 
    void foo_impl(T& t) {
        size_t so=vec.size();
        vec.reserve(sz*100);
        for (size_t i=sz; sz<sz*100;++sz) {...}
    }
}

void foo(std::vector& t) { foo_impl(t);}
void foo(my_vector& t) { foo_impl(t);}

The first thing you should do is go and write or get gsl::span . A gsl::span<const T> is a top-notch replacement for a std::vector<T,A> const& .

The next thing you often want to do is to append data into a vector.

template<class T>
struct sink_t {
  using impl=std::function<void(T&&)>;
  impl fn;
  using res_fn=std::function<void(std::size_t)>;
  res_fn res;
  void reserve(std::size_t n) { if(res) res(n); }
  void push_back(T const& t) {
    push_back( T(t) );
  }
  void push_back( T&& t ) {
    fn(std::move(t));
  }
  void operator()(T&& t)const { push_back(std::move(t)); }
  sink_t(impl f, res r={}):fn(std::move(f)), res(std::move(r)) {}
  template<class A>
  sink_t( std::vector<T, A>& v ):
    fn(
      [&v](T&& tin){
        v.push_back( std::move(tin) );
      }
    ),
    res(
      [&v](std::size_t n){v.reserve(n);}
    )
  {}
  sink_t( my_vector<T>& v ):
    fn(
      [&v](T&& tin){
        v.push_back( std::move(tin) );
      }
    ),
    res(
      [&v](std::size_t n){v.reserve(n);}
    )
  {}
  sink_t( T& t ):
    fn( [&t]( T&& tin ) { t = std::move(tin); } )
  {}
  sink_t( T* t ):
    fn( t?impl([t]( T&& tin ) { *t = std::move(tin); }):impl() )
  {}
  explicit operator bool() const { return (bool)fn; }
};

now if you have code that shoves things into a vector<T> by push_back , have it instead take a sink_t<T> .

This, honestly, should deal with 99/100 reasonable cases.

The exception is if you have code that both reads and writes from a container, which is usually a bad sign.

In those cases, you could go and write a method-for-method vector type eraser. But the cost to doing this is probably not worth it.

I'd propose that you use the above two techniques and see how much of your code becomes trivial to make flexible.

A gsl::span<T> is a view of (part of or a whole) contiguous buffer of T . A gsl::span<T const> is basically all the parts of a std::vector<T> const& you care about without requiring that it be actually stored in a particular structure.

A sink_t<T> represents "a place to throw T s into". I assume moving a T is cheap in the above code, and std::function has a touch more overhead than manual type erasure involving function pointers, but it should be pretty decent. I gave it .reserve and .push_back because that should cover most of the code you want; you could add .insert_back( Iterator, Iterator ) and .move_into_back( Container&& ) and .insert_back( Container const& ) , but I wouldn't add .end() as that is a pretty simple code transformation to replace vec.insert( vec.end(), start, finish ) with vec.insert_back( start, finish ) .

A final possibility is that your code is doing a std::vector<T>& vec , and all it does is assign to that vec (or clear it and assign to it). That is a bit of code smell, but you can fix it by having a thin wrapper that takes a container, clears it, then passes it to a sink-based version of the function.

This is a technique I've used in my own code to abstract out "where I'm putting stuff". It works reasonably well.


As an extra fun thing, if your have code that does pipeline processing like this:

template<class In, class Out>
using pipeline = std::function< void( In&&, sink_t<Out> ) >;

you can compose them

template<class In, class Out>
sink_t<In> operator|( pipeline<In, Out> pipe, sink_t<Out> sink ) {
  return
    [pipe=std::move(pipe), sink=std::move(sink)]( In&& in ){
      pipe( std::move(in), sink );
    };
}
template<class In, class Mid, class Out>
pipeline<In, Out> operator|( pipeline<In, Mid> lhs, pipeline<Mid, Out> rhs ) {
  return [lhs=std::move(lhs), rhs=std::move(rhs)]( In&& in, sink_t<Out>  sink){
    return lhs( std::move(in), std::move(rhs)|std::move(sink) );
  };
}

which is pretty fun.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM