Как передать полиморфный объект в алгоритм STL

в 10:58, , рубрики: c++, std::ref, Блог компании Инфопульс Украина, Компиляторы, Программирование

Как мы можем прочесть в первой главе книги Effective C++, язык С++ является по сути своей объединением 4 разных частей:

  • Процедурная часть, доставшаяся в наследство от языка С
  • Объектно-ориентировання часть
  • STL, пытающийся следовать функциональной парадигме
  • Шаблоны

Эти четыре, по сути, подъязыка составляют то, что мы называем единым языком С++. Поскольку все они объединены в одном языке, то это даёт им возможность взаимодействовать. Это взаимодействие порой порождает интересные ситуации. Сегодня мы рассмотрим одну из них — взаимодействие объектно-ориентированной модели и STL. Оно может принимать разнообразные формы и в данной статье мы рассмотрим передачу полиморфных функциональных объектов в алгоритмы STL. Эти два мира не всегда хорошо контачат, но мы можем построить между ними достаточно неплохой мостик.

image

Полиморфные функциональные объекты — что это?

Под функциональным объектом в С++ я понимаю объект, у которого можно вызвать operator(). Это может быть лямбда-функция или функтор. Полиморфность может означать различные вещи в зависимости от языка программирования и контекста, но здесь я буду называть полиморфными объекты тех классов, у которых применяется наследование и виртуальные методы. То есть полиморфный функциональный объект, это что-то типа:

struct Base
{
    int operator()(int) const
    {
        method();
        return 42;
    }
    virtual void method() const { std::cout << "Base class called.n"; }
};

Данный функциональный объект не делает ничего полезного, но это даже хорошо, ведь реализация его методов не будет отвлекать нас от основной задачи — передать его наследника в алгоритм STL. А наследник будет переопределять виртуальный метод:

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

Давайте попробуем передать наследника в STL-алгоритм тривиальным способом, вот так:

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

Что бы вы думали выведет этот код?

Вот это

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

Странно, правда? Мы передали алгоритму объект класса Derived, с перегруженным виртуальным методом, но алгоритм решил вызвать вместо него метод базового класса. Чтобы понять, что произошло, давайте взглянем на прототип функции std::transform:

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

Посмотрите внимательно на её последний параметр (Function f) и обратите внимание, что он передаётся по значению. Как объясняется в главе 20 той же книги Effective C++, полиморфные объекты «срезаются», когда мы передаём их по значению: даже если ссылка на Base const& указывает на объект типа Derived, создание копии base создаёт объект типа Base, а не объект типа Derived.

Таким образом, нам нужен способ передать STL-алгоритму ссылку на полиморфный объект, а не на его копию.

Как это сделать?

Давайте завернём наш объект в ещё один

Эта мысль вообще приходит первой: «Проблема? Давайте решим её с помощью добавления косвенности!» Если наш объект должен быть сначала передан по ссылке, а STL-алгоритм принимает лишь объекты по значению, то мы можем создать промежуточный объект, который будет хранить ссылку на нужный нам полиморфный объект, а вот сам этот объект уже может передаваться по значению.

Простейший путь сделать это — использовать лямбда-функцию:

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

Теперь код выводит следующее:

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

Это работает, но обременяет код лямбда-функцией, которая хоть и достаточно коротка, но всё-же написана не для изящества кода, а лишь по техническим причинам.
Кроме того, в реальном коде она может выглядеть куда длиннее:

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

Избыточный код, использующий функциональную парадигму в качестве костыля.

Компактное решение: использовать std::ref

Есть и другой способ передачи полиморфного объекта по значению и заключается он в использовании std::ref

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

Эффект будет такой же, как и от лямбда-функции:

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

Возможно, сейчас у вас возникает вопрос «А почему?». У меня, например, он возник. Во-первых, как это вообще скомпилировалось? std::ref возвращает объект типа std::reference_wrapper, который моделирует ссылку (с тем лишь исключением, что ей можно переприсвоить другой объект с помощью использования operator=). Как же std::reference_wrapper может играть роль функционального объекта? Я посмотрел документацию по std::reference_wrapper на cppreference.com и нашел вот это:

std::reference_wrapper::operator()

Calls the Callable object, reference to which is stored. This function is available only if the stored reference points to a Callable object.

То есть это такая специальная фича в std::reference_wrapper: если std::ref принимает функциональный объект типа F, то возвращаемый объект-имитатор ссылки тоже будет функционального типа и его operator() будет вызывать operator() типа F. В точности то, что нам и было необходимо.

Мне данное решение кажется лучшим, чем использование лямбда-функций, ведь тот же результат достигается более простым и лаконичным кодом. Возможно, существуют и другие решения данной проблемы — буду рад увидеть их в комментариях.

Автор: tangro

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js