Как мы можем прочитать в первой главе книги Эффективный С++ язык C++, по сути, представляет собой комбинацию четырех различных частей:
- Процедурная часть, унаследованная от языка C
- Объектно-ориентированная часть
- STL пытается следовать функциональной парадигме
- Шаблоны
Поскольку все они объединены одним языком, это дает им возможность взаимодействовать.
Такое взаимодействие иногда порождает интересные ситуации.
Сегодня мы рассмотрим один из них — взаимодействие объектно-ориентированной модели и STL. Это может принимать разные формы, и в этой статье мы рассмотрим передачу полиморфных функциональных объектов в алгоритмы STL. Эти два мира не всегда хорошо общаются, но мы можем построить между ними довольно хороший мост.
Полиморфные функциональные объекты – что это такое?
Под функциональным объектом в C++ я подразумеваю объект, для которого можно вызвать оператор().Это может быть лямбда-функция или функтор.
Полиморфность может означать разные вещи в зависимости от языка программирования и контекста, но здесь я буду называть полиморфными объекты классов, использующих наследование и виртуальные методы.
То есть полиморфный функциональный объект — это что-то вроде:
Этот функциональный объект не делает ничего полезного, но это даже хорошо, поскольку реализация его методов не будет отвлекать нас от основной задачи — передачи его наследника алгоритму STL. И наследник переопределит виртуальный метод:struct Base { int operator()(int) const { method(); return 42; } virtual void method() const { std::cout << "Base class called.\n"; } };
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);
}
Как вы думаете, что выведет этот код? Этот Вызывается базовый класс.
Вызывается базовый класс.
Вызывается базовый класс.
Странно, не так ли? Мы передали алгоритму объект класса Derived с перегруженным виртуальным методом, но алгоритм решил вместо этого вызвать метод базового класса.
Чтобы понять, что произошло, давайте посмотрим на прототип функции std::transform: template< typename InputIterator, typename OutputIterator, typename Function>
OutputIt transform(InputIterator first, InputIterator last, OutputIterator out, Function f);
Посмотрите внимательно на ее последний параметр (функция f) и обратите внимание, что он передается по значению.
Как поясняется в главе 20 той же книги «Эффективный C++», полиморфные объекты «обрезаются», когда мы передаем их по значению: даже если ссылка Base const& указывает на объект типа Derived, создание копии 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, который моделирует ссылку (за исключением того, что ее можно переназначить другому объекту с помощью оператора =).
Как std::reference_wrapper может играть роль функционального объекта? Я просмотрел документацию std::reference_wrapper по адресу cppreference.com и нашел это:
std::reference_wrapper::operator() Вызывает объект Callable, ссылка на который сохраняется.То есть это особенность std::reference_wrapper: если std::ref принимает функциональный объект типа F, то возвращаемый ссылочный объект также будет функционального типа и его оператор() вызовет оператор() типа Ф.Эта функция доступна только в том случае, если сохраненная ссылка указывает на вызываемый объект.
Именно то, что нам нужно.
Мне это решение кажется лучше, чем использование лямбда-функций, потому что тот же результат достигается с помощью более простого и лаконичного кода.
Могут быть и другие решения этой проблемы — буду рад их увидеть в комментариях.
Теги: #std::ref #программирование #C++ #компиляторы
-
Пикеринг, Уильям Генри
19 Oct, 24 -
3 Года Слепого Программирования. Часть 2
19 Oct, 24 -
Сайт Пенсионного Фонда России
19 Oct, 24