Как мы можем прочесть в первой главе книги Effective C++, язык С++ является по сути своей объединением 4 разных частей:
Эти четыре, по сути, подъязыка составляют то, что мы называем единым языком С++. Поскольку все они объединены в одном языке, то это даёт им возможность взаимодействовать. Это взаимодействие порой порождает интересные ситуации. Сегодня мы рассмотрим одну из них — взаимодействие объектно-ориентированной модели и STL. Оно может принимать разнообразные формы и в данной статье мы рассмотрим передачу полиморфных функциональных объектов в алгоритмы STL. Эти два мира не всегда хорошо контачат, но мы можем построить между ними достаточно неплохой мостик.
Под функциональным объектом в С++ я понимаю объект, у которого можно вызвать operator(). Это может быть лямбда-функция или функтор. Полиморфность может означать различные вещи в зависимости от языка программирования и контекста, но здесь я буду называть полиморфными объекты тех классов, у которых применяется наследование и виртуальные методы. То есть полиморфный функциональный объект, это что-то типа:
Данный функциональный объект не делает ничего полезного, но это даже хорошо, ведь реализация его методов не будет отвлекать нас от основной задачи — передать его наследника в алгоритм STL. А наследник будет переопределять виртуальный метод:
Давайте попробуем передать наследника в STL-алгоритм тривиальным способом, вот так:
Что бы вы думали выведет этот код?
Странно, правда? Мы передали алгоритму объект класса Derived, с перегруженным виртуальным методом, но алгоритм решил вызвать вместо него метод базового класса. Чтобы понять, что произошло, давайте взглянем на прототип функции std::transform:
Посмотрите внимательно на её последний параметр (Function f) и обратите внимание, что он передаётся по значению. Как объясняется в главе 20 той же книги Effective C++, полиморфные объекты «срезаются», когда мы передаём их по значению: даже если ссылка на Base const& указывает на объект типа Derived, создание копии base создаёт объект типа Base, а не объект типа Derived.
Таким образом, нам нужен способ передать STL-алгоритму ссылку на полиморфный объект, а не на его копию.
Как это сделать?
Эта мысль вообще приходит первой: «Проблема? Давайте решим её с помощью добавления косвенности!» Если наш объект должен быть сначала передан по ссылке, а STL-алгоритм принимает лишь объекты по значению, то мы можем создать промежуточный объект, который будет хранить ссылку на нужный нам полиморфный объект, а вот сам этот объект уже может передаваться по значению.
Простейший путь сделать это — использовать лямбда-функцию:
Теперь код выводит следующее:
Это работает, но обременяет код лямбда-функцией, которая хоть и достаточно коротка, но всё-же написана не для изящества кода, а лишь по техническим причинам.
Кроме того, в реальном коде она может выглядеть куда длиннее:
Избыточный код, использующий функциональную парадигму в качестве костыля.
Есть и другой способ передачи полиморфного объекта по значению и заключается он в использовании std::ref
Эффект будет такой же, как и от лямбда-функции:
Возможно, сейчас у вас возникает вопрос «А почему?». У меня, например, он возник. Во-первых, как это вообще скомпилировалось? std::ref возвращает объект типа std::reference_wrapper, который моделирует ссылку (с тем лишь исключением, что ей можно переприсвоить другой объект с помощью использования operator=). Как же std::reference_wrapper может играть роль функционального объекта? Я посмотрел документацию по std::reference_wrapper на cppreference.com и нашел вот это:
Мне данное решение кажется лучшим, чем использование лямбда-функций, ведь тот же результат достигается более простым и лаконичным кодом. Возможно, существуют и другие решения данной проблемы — буду рад увидеть их в комментариях.
- Процедурная часть, доставшаяся в наследство от языка С
- Объектно-ориентировання часть
- STL, пытающийся следовать функциональной парадигме
- Шаблоны
Эти четыре, по сути, подъязыка составляют то, что мы называем единым языком С++. Поскольку все они объединены в одном языке, то это даёт им возможность взаимодействовать. Это взаимодействие порой порождает интересные ситуации. Сегодня мы рассмотрим одну из них — взаимодействие объектно-ориентированной модели и STL. Оно может принимать разнообразные формы и в данной статье мы рассмотрим передачу полиморфных функциональных объектов в алгоритмы STL. Эти два мира не всегда хорошо контачат, но мы можем построить между ними достаточно неплохой мостик.
Полиморфные функциональные объекты — что это?
Под функциональным объектом в С++ я понимаю объект, у которого можно вызвать 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.
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()То есть это такая специальная фича в std::reference_wrapper: если std::ref принимает функциональный объект типа F, то возвращаемый объект-имитатор ссылки тоже будет функционального типа и его operator() будет вызывать operator() типа F. В точности то, что нам и было необходимо.
Calls the Callable object, reference to which is stored. This function is available only if the stored reference points to a Callable object.
Мне данное решение кажется лучшим, чем использование лямбда-функций, ведь тот же результат достигается более простым и лаконичным кодом. Возможно, существуют и другие решения данной проблемы — буду рад увидеть их в комментариях.
Комментарии (5)
KruLV
25.04.2018 11:52Самое первое что в голову приходит — это std::ref. Прочитал заголовок, ожидал срыва покровов, думал мне расскажут почему его на самом деле нельзя использовать.
slonopotamus
Более лучшее решение — включать мозг при проектировании API. К сожалению, для C++ уже всё потрачено, в него добавлено чудовищное количество граблей. Включая подобные описанным в посте — из-за того что написать код который скопирует объект проще чем код который НЕ скопирует, это самое копирование вылезает тут и там.
Замечу что в C такой проблемы бы не было, там бы transform принимал указатель на функцию и void* userdata, не имея проблем с полиморфизмом.
tangro Автор
Не имя проблем с полиморфизмом и имея проблемы с void*.
slonopotamus
C тоже имеет кучу проблем. Там где в C используется void* + функция, C++ может предложить более типобезопасный указатель/ссылку на объект.
Antervis
«нет обобщенного программирования — нет проблем»?