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