📜 ⬆️ ⬇️

How to transfer a polymorphic object to the STL algorithm

As we can read in the first chapter of the book Effective C ++ , the C ++ language is essentially a union of 4 different parts:


These four, in essence, sublanguages ​​make up what we call the unified C ++ language. Since all of them are united in one language, this gives them the opportunity to interact. This interaction sometimes generates interesting situations. Today we look at one of them - the interaction of the object-oriented model and STL. It can take various forms and in this article we will consider the transfer of polymorphic functional objects to STL algorithms. These two worlds are not always well contacted, but we can build a fairly good bridge between them.

image

Polymorphic functional objects - what is it?


By a functional object in C ++, I mean an object that can call operator (). This can be a lambda function or a functor. Polymorphism can mean different things depending on the programming language and context, but here I will call polymorphic objects of those classes that use inheritance and virtual methods. That is, a polymorphic functional object is something like:
')
struct Base { int operator()(int) const { method(); return 42; } virtual void method() const { std::cout << "Base class called.\n"; } }; 

This functional object does nothing useful, but it is even good, because the implementation of its methods will not distract us from the main task - to transfer its successor to the STL algorithm. And the heir will override the virtual method:

 struct Derived : public Base { void method() const override { std::cout << "Derived class called.\n"; } }; 

Let's try to pass a successor to the STL algorithm in a trivial way, like this:

 void f(Base const& base) { std::vector<int> v = {1, 2, 3}; std::transform(begin(v), end(v), begin(v), base); } int main() { Derived d; f(d); } 

What would you think would output this code?

This
Base class called.
Base class called.
Base class called.

Strange, right? We passed a Derived class object to the algorithm, with an overloaded virtual method, but the algorithm decided to call the base class method instead. To understand what happened, let's take a look at the prototype of the std :: transform function:

 template< typename InputIterator, typename OutputIterator, typename Function> OutputIt transform(InputIterator first, InputIterator last, OutputIterator out, Function f); 

Look carefully at its last parameter (Function f) and note that it is passed by value. As explained in Chapter 20 of the same book, Effective C ++, polymorphic objects are “cut off” when we pass them by value: even if the reference to Base const & points to an object of type Derived, creating a copy of base creates an object of type Base rather than an object of type Derived.

Thus, we need a way to pass a reference to the polymorphic object, and not to its copy, to the STL algorithm.

How to do it?

Let's wrap our object in one more.


This thought generally comes first: “The problem? Let's solve it by adding indirection! ”If our object must first be passed by reference, and the STL algorithm accepts only objects by value, then we can create an intermediate object that will store a reference to the polymorphic object we need, but this one the object can already be passed by value.

The easiest way to do this is to use the lambda function:

 std::transform(begin(v), end(v), begin(v), [&base](int n){ return base(n); } 

Now the code displays the following:

Derived class called.
Derived class called.
Derived class called.


It works, but it burdens the code of the lambda function, which, although rather short, is still not written for the grace of the code, but only for technical reasons.
In addition, in real code it may look much longer:

 std::transform(begin(v), end(v), begin(v), [&base](module::domain::component myObject){ return base(myObject); } 

Redundant code using the functional paradigm as a crutch.

Compact solution: use std :: ref


There is another way to transfer a polymorphic object by value, and it is to use std :: ref

 std::transform(begin(v), end(v), begin(v), std::ref(base)); 

The effect will be the same as for the lambda function:

Derived class called.
Derived class called.
Derived class called.


Perhaps now you have the question "Why?". For example, I have it. First, how did it compile at all? std :: ref returns an object of type std :: reference_wrapper, which models the link (with the only exception that it can be reassigned to another object using operator =). So how can std :: reference_wrapper play the role of a functional object? I looked at the documentation for std :: reference_wrapper at cppreference.com and found this:
std :: reference_wrapper :: operator ()

Calls the Callable object, reference to which is stored. Callable object.
That is, it is such a special feature in std :: reference_wrapper: if std :: ref accepts a functional object of type F, then the returned simulator object of the link will also be of functional type and its operator () will call operator () of type F. Exactly what we needed.

This solution seems to me better than the use of lambda functions, because the same result is achieved with a simpler and more concise code. Perhaps there are other solutions to this problem - I will be glad to see them in the comments.

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


All Articles