Syntactic sugar: automatic creation of simple functional objects

I have to implement a set of class templates and two special variables, _1 and _2 .

They must make the following legal code:

 // Sort ascending std::sort(a, a+5, _1 > _2); // Output to a stream std::for_each(a, a+5, std::cout << _1 << " "); // Assign 100 to each element std::for_each(a, a+5, _1 = 100); // Print elements increased by five 5 std::transform(a, a+5, std::ostream_iterator<int>(std::cout, " "), _1 + 5); 

I believe that _1 * 5 should also give a unary function, as well as _1 / 5, etc.

  • Not allowed to increase
  • Lambda not allowed

Now I have very little experience with templates and template metaprogramming, so I don’t even know where to start and how the template structure of my classes should look. I am especially confused because I don’t know if inside my class templates I have to write implementations for all of these operator= , operator>> , operator+ , ...- , ...* , .../ separately - or is there a more general one way to do it.

I would be especially grateful for the answer with an example of the implementation of these operators; templates still seem like a big mess to me.

+4
source share
2 answers

Well! Indeed, this is a difficult home problem! But, this is also a very good problem that you can work on and learn from.

I think the best way to answer this question is to start with simple use cases and gradually build up your solution.

For example, suppose 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); 

Obviously, you want to resolve the following use case:

 std::for_each(vec.cbegin(), vec.cend(), _1); 

But how to resolve this? First you need to define _1 , and then you need to implement “something” overloading the function call statement for type _1 .

The way that Boost Lambda and Boost Bind define placeholders _1 , _2 , ... is to force them to have a dummy type. For example, object _1 can be of type placeholder1_t :

 struct placeholder1_t { }; placeholder1_t _1; struct placeholder2_t { }; placeholder2_t _2; 

Such a "dummy type" is often informally called a tag type. There are many C ++ and indeed STL libraries that rely on tag types (e.g. std::nothrow_t ). They are used to select the “correct” overload function to execute. In fact, dummy objects are created that have a tag type, and they are passed to the function. The function does not use the dummy object in any way (in fact, most of the time the parameter name is not even defined for it), but thanks to this additional parameter, the compiler can select the correct overload for the call.

Extend the definition of placeholder1_t by adding overloads to the function call operator. Remember that we want it to accept something, so overloads of the function call operator will themselves be masked by the type of the argument:

 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; } }; 

What is it! Our simplest use cases will now compile and run:

 std::for_each(vec.cbegin(), vec.cend(), _1); 

Of course, this is basically equal to no-op.

Now we are working on _1 + 5 . What should this expression do? It must return a unary functional object, which, when called with an argument (of some unknown type), causes the argument to be plus 5. Applying this more general, the expression is an object of a unary functional object + . The returned object itself is a unary functional object.

You must specify the type of the returned object. It will be a template with two parameters of the template type: the unary functional type and the type of the object that is 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 a partial application of the binary + operator. Instances of this type need a copy of the unary functional object (of type UnaryFnT ) and another object (of 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) { } }; 

Good. A function call statement must also be overloaded to resolve any argument. We will use the C ++ 11 decltype to refer to the type of expression, since we do not know that this is in advance:

 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; } }; 

This starts to get complicated, but there are no surprises in this code. Essentially, he says that the function call operator is overloaded to accept almost any argument. Then it will call m_fn (the unary function object) in the argument and add m_obj to the result. The return type is the declaration type m_fn(arg) + m_obj .

Now that the type is defined, we can write an overload of the binary operator + by casting 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); } 

Now we 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

That is basically all you need to do to solve the problem. Think about how you can write custom function types, instances of which can be returned by operator overloads.

EDIT: Improved overloading of function call statements by using pass-by-reference.

EDIT2: In some cases, it is necessary to keep a reference to the object, rather than a copy of it. For example, to place std::cout << _1 , you would need to store the link to std::cout in the resulting function object, because the copy constructor std::ios_base is private, and it is impossible to copy construction objects of any class derived from std::ios_base including std::ostream .

To enable std::cout << _1 , you can write the template ref_insert_unary_partfn_t . A template like the unary_plus_object_partfn_t example above would be a template for an object type and a unary function type:

 template <typename ObjT, typename UnaryFnT> struct ref_insert_unary_partfn_t; 

In instances of instances of this template, you will need to save 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 function operator overloads as before, as well as insert operator overloads << .

In the case of std::cout << _1 returned object will be of type ref_insert_unary_partfn_t<std::basic_ostream<char>, placeholder1_t> .

+5
source

A simple example:

 template <typename T> class Parameter { }; template <typename T> struct Ascending { bool operator()(T left, T right) { return left < right; } }; template <typename T> Ascending<T> operator > (Parameter<T> p1, Parameter<T> p2) { return Ascending<T>(); } int main() { std::vector<int> vec; vec.push_back(3); vec.push_back(6); vec.push_back(7); vec.push_back(2); vec.push_back(7); std::vector<int>::iterator a = vec.begin(); Parameter<int> _1; Parameter<int> _2; std::sort(a, a+4, _1 > _2); } 
+2
source

Source: https://habr.com/ru/post/1385548/


All Articles