Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it wrong to use templates with the implicit assumption that certain member functions of the parameterized type will be defined?

Say you write a really bad class

template <typename T>
class IntFoo
{
  T container ;
public:
  void add( int val )
  {
    // made an assumption that
    // T will have a method ".push_front".
    container.push_front( val ) ;
  }
} ;

Ignore the fact that the class assumes the container will be something<int>, instead pay attention to the fact that

IntFoo< list<int> > listfoo ;
listfoo.add( 500 ) ; // works

IntFoo< vector<int> > intfoo;
//intfoo.add( 500 ) ; // breaks, _but only if this method is called_..

In general, is it ok to call a member function of a parameterized type like this? Is this bad design? Does this (anti)pattern have a name?

like image 824
bobobobo Avatar asked Oct 03 '12 18:10

bobobobo


1 Answers

This is perfectly fine and called compile-time duck typing and employed at all kinds of places all over the standard library itself. And seriously, how would you do anything useful with a template without assuming the template argument to support certain functionalities?

Let's take a look at any algorithm in the stdlib, e.g., std::copy:

template<class InIt, class OutIt>
OutIt copy(InIt first, Init last, OutIt out){
  for(; first != last; ++first)
    *out++ = *first;
  return out;
}

Here, an object of type InIt is assumed to support operator*() (for indirection) and operator++() for advancing the iterator. For an object of type OutIt, it's assumed to support operator*() aswell, and operator++(int). A general assumption is also that whatever is returned from *out++ is assignable (aka convertible) from whatever *first yields. Another assumption would be that both InIt and OutIt are copy constructible.

Another place this is used is in any standard container. In C++11, when you use std::vector<T>, T needs to be copy constructible if and only if you use any member function that requires a copy.

All of this makes it possible for user-defined types to be treated the same as a built-in type, i.e., they're fist-class citizens of the language. Let's take a look at some algorithms again, namely ones that take a callback that is to be applied on a range:

template<class InIt, class UnaryFunction>
InIt for_each(InIt first, InIt last, UnaryFunction f){
  for(; first != last; ++first)
    f(*first);
  return first;
}

InIt is assumed to support the same operations again as in the copy example above. However, now we also have UnaryFunction. Objects of this type are assumed to support the post-fix function call notation, specifically with one argument (unary). Further is assumed that this the parameter of this function call is convertible from whatever *first yields.

The typical example for the usage of this algorithm is with a plain function:

void print(int i){ std::cout << i << " "; }

int main(){
  std::vector<int> v(5); // 5 ints
  for(unsigned i=0; i < v.size(); ++i)
    v[i] = i;
  std::for_each(v.begin(), v.end(), print); // prints '0 1 2 3 4 '
}

However, you can also use function objects for this - a user-defined type that overloads operator():

template<class T>
struct generate_from{
  generate_from(T first) : _acc(first) {}
  T _acc;
  void operator()(T& val){ val = _acc++; }
};

int main(){
  std::vector<int> v(5); // 5 ints
  // yes, there is std::iota. shush, you.
  std::for_each(v.begin(), v.end(), generate_from<int>(0)); // fills 'v' with [0..4]
  std::for_each(v.begin(), v.end(), print); // prints '0 1 2 3 4 '
}

As you can see, my user-defined type generate_from can be treated exactly like a function, it can be called as if it was a function. Note that I make several assumptions on T in generate_from, namely it needs to be:

  • copy-constructible (in the ctor)
  • post-incrementable (in the operator())
  • copy-assignable (in the operator())
like image 95
Xeo Avatar answered Sep 28 '22 04:09

Xeo