Well! That is a tricky homework problem, indeed! But, it's also a very good problem to work on and to learn from.
I think that the best way to answer this is for you to start off with simple use cases and incrementally build up your solution.
For example, suppose that you have the following std::vector<int>
to work with:
std::vector<int> vec;
vec.push_back(4);
vec.push_back(-8);
vec.push_back(1);
vec.push_back(0);
vec.push_back(7);
You'll obviously want to allow the following use case:
std::for_each(vec.cbegin(), vec.cend(), _1);
But how to allow this? First you'll need to define _1
and then you'll need to implement an "anything goes" overload of the function call operator for the type of _1
.
The way that Boost Lambda and Boost Bind define placeholders objects _1
, _2
, ... is to make them have a dummy type. For example, the _1
object might have the type placeholder1_t
:
struct placeholder1_t { };
placeholder1_t _1;
struct placeholder2_t { };
placeholder2_t _2;
Such a "dummy type" is frequently, informally called a tag type. There are many C++ libraries and indeed the STL that rely on tag types (e.g. std::nothrow_t
). They are used to pick the "right" function overload to execute. Essentially, dummy objects are created having a tag type and these are passed into a function. The function does not use the dummy object in any way (in fact, most of the time a parameter name is not even specified for it), but by the existence of that extra parameter, the compiler is able to pick the correct overload to call.
Let's extend the definition of placeholder1_t
by adding overloads of the function call operator. Remember that we want it to accept anything, so the overloads of the function call operator will themselves be templated on the argument type:
struct placeholder1_t
{
template <typename ArgT>
ArgT& operator()(ArgT& arg) const {
return arg;
}
template <typename ArgT>
const ArgT& operator()(const ArgT& arg) const {
return arg;
}
};
That's it! Our simplest of use cases will now compile and run:
std::for_each(vec.cbegin(), vec.cend(), _1);
Of course, it basically amounts to a no-op.
Let's now work on _1 + 5
. What should that expression do? It should return a unary functional object that, when invoked with an argument (of some unknown type), the result is that argument plus 5. Making this more generic, the expression is unary-functional-object +
object. The returned object is itself a unary functional object.
The type of the returned object needs to be defined. It will be a template with two template type parameters: the unary functional type and the type of the object that is being added to the result of the unary functional:
template <typename UnaryFnT, typename ObjT>
struct unary_plus_object_partfn_t;
"partfn" refers to a functional type representing partial application of the binary +
operator. Instances of this type need a copy of the unary functional object (having type UnaryFnT
) and the other object (having type ObjT
):
template <typename UnaryFnT, typename ObjT>
struct unary_plus_object_partfn_t
{
UnaryFnT m_fn;
ObjT m_obj;
unary_plus_object_partfn_t(UnaryFnT fn, ObjT obj)
: m_fn(fn), m_obj(obj)
{
}
};
Okay. The function call operator also needs to be overloaded to allow for any argument. We'll use the C++11 decltype
feature to refer to the type of an expression as we don't know what it is beforehand:
template <typename UnaryFnT, typename ObjT>
struct unary_plus_object_partfn_t
{
UnaryFnT m_fn;
ObjT m_obj;
unary_plus_object_partfn_t(UnaryFnT fn, ObjT obj)
: m_fn(fn), m_obj(obj)
{
}
template <typename ArgT>
auto operator()(ArgT& arg) const -> decltype(m_fn(arg) + m_obj) {
return m_fn(arg) + m_obj;
}
template <typename ArgT>
auto operator()(const ArgT& arg) const -> decltype(m_fn(arg) + m_obj) {
return m_fn(arg) + m_obj;
}
};
It's starting to get complicated, but there are no surprises in this code. It essentially says that the function call operator is overloaded to accept practically any argument. It will then call m_fn
(the unary functional object) on the argument and add m_obj
to the result. The return type is the decltype of m_fn(arg) + m_obj
.
Now that the type is defined, we can write the overload of binary operator +
accepting an object of type placeholder1_t
on the left:
template <typename ObjT>
inline unary_plus_object_partfn_t<placeholder1_t, ObjT> operator+(const placeholder1_t& fn, ObjT obj)
{
return unary_plus_object_partfn_t<placeholder1_t, ObjT>(fn, obj);
}
We now can compile and run the second use case:
std::transform(vec.cbegin(), vec.cend(), std::ostream_iterator<int>(std::cout, " "), _1 + 5);
std::cout << std::endl;
which outputs:
9 -3 6 5 12
This is basically all that you need to do to solve the problem. Think about how you can write custom functional types, instances of which can be returned by overloads of operators.
EDIT: Improved the overloads of function call operators by employing pass-by-reference.
EDIT2: In some cases it will be necessary to store a reference to an object rather than a copy of it. For example, to accommodate std::cout << _1
, you will need to store a reference to std::cout
in the resulting functional object because the std::ios_base
copy constructor is private, and it is impossible to copy construct objects of any class derived from std::ios_base
including std::ostream
.
To allow for std::cout << _1
, you might want to write a ref_insert_unary_partfn_t
template. Such a template, like the example of unary_plus_object_partfn_t
above, would be templated on an object type and a unary functional type:
template <typename ObjT, typename UnaryFnT>
struct ref_insert_unary_partfn_t;
Instances of instantiations of this template will need to store a reference to an object of type ObjT
as well as a copy of a unary functional object of type UnaryFnT
:
template <typename ObjT, typename UnaryFnT>
struct ref_insert_unary_partfn_t
{
ObjT& m_ref;
UnaryFnT m_fn;
ref_insert_unary_partfn_t(ObjT& ref, UnaryFnT fn)
: m_ref(ref), m_fn(fn)
{
}
};
Add overloads of the function call operator as before as well as overloads of the insertion operator, <<
.
In the case of std::cout << _1
, the returned object would have the type ref_insert_unary_partfn_t<std::basic_ostream<char>, placeholder1_t>
.