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()
)