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:
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:
operator())operator())If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With