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?
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:Here, an object of type
InItis assumed to supportoperator*()(for indirection) andoperator++()for advancing the iterator. For an object of typeOutIt, it’s assumed to supportoperator*()aswell, andoperator++(int). A general assumption is also that whatever is returned from*out++is assignable (aka convertible) from whatever*firstyields. Another assumption would be that bothInItandOutItare copy constructible.Another place this is used is in any standard container. In C++11, when you use
std::vector<T>,Tneeds 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:
InItis assumed to support the same operations again as in thecopyexample above. However, now we also haveUnaryFunction. 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*firstyields.The typical example for the usage of this algorithm is with a plain function:
However, you can also use function objects for this – a user-defined type that overloads
operator():As you can see, my user-defined type
generate_fromcan be treated exactly like a function, it can be called as if it was a function. Note that I make several assumptions onTingenerate_from, namely it needs to be:operator())operator())