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

1

5

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?

Doubleton answered 3/10, 2012 at 18:28 Comment(3)
The name is called template method pattern or have i mistaken it?Drastic
@ahenderson: You are mistaken. Confusingly, the Template Method Pattern has nothing to do with templates.Hagans
It's one way of dealing with the fact that a method cannot be virtual and template at the same time.Merari
S
12

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())
Strahan answered 3/10, 2012 at 18:38 Comment(3)
This begs to mention the failed first attempt to introduce concepts.Hagans
So where do you document your expectations for T? For example, in a comment? // T SHOULD BE A POINTER TYPE, // T SHOULD SUPPORT T->getAverage(),Doubleton
@bobobobo: Normally through the parameter name. template<class RanIt> documents that Iter should support random-access iterator operations. Other than that, documentation. See the standard with all the predicates the algorithms take. In the documentation they only say what operations Pred should support.Strahan

© 2022 - 2024 — McMap. All rights reserved.