Многообразие функциональных обёрток

5ae272b3c2c086292e6cd88cb9029f89

A polymorphic function object wrapper

В далёком 2002-ом комитет по стандартизации C++ посетил пропозал, предлагавший ввести шаблонный класс, некий обобщенный «указатель на функцию», способный работать как с простыми указателями на функции, указателями на методы классов, так и с произвольными функциональными объектами [1].

В качестве мотивации к принятию он приводил несколько весомых юзкейсов: колбэки и функции высших порядков.

Колбэки

Без function

class mouse_move_listener {
public:
  virtual void on_mouse_move(int x, int y) = 0;
};

class mouse_loc_printer : public mouse_move_listener {
public:
  virtual void on_mouse_move(int x, int y) {
    std::cout << '(' << x << ", " << y << "\n";
  }
};

mouse_move_listener* move_listener;

void fire_mouse_move(int x, int y) {
  if (move_listener)
    move_listener->on_mouse_move(x, y);
}

С function

std::function on_mouse_move;

void fire_mouse_move(int x, int y, std::function

Функции высших порядков

function arithmetic_operation(char k) {
  switch (k) {
    case '+': return plus();
    case '-': return minus();
    case '*': return multiplies();
    case '/': return divides();
    case '%': return modulus();
    default: assert(0);
  }
}

Комитет долго думал и решил: фиче быть! Так в стандартную библиотеку C++11 вошел всем нам хорошо знакомый класс function.

И всё было бы хорошо. Но этот класс вышел в свет неадаптированным к наступившим реалиям.

Что если мы захотим захватить в лямбду что-нибудь некопируемое и лишь перемещаемое? Например, unique_ptr? (godbolt)

#include 
#include 

struct widget { };

int main() {
  auto w_ptr = std::make_unique();
  std::function f = [w_ptr = std::move(w_ptr)] { /*...*/ };
}

Этот код просто не скомпилируется:

/opt/.../function.h:439:69: error: std::function target must be copy-constructible
  439 | static_assert(is_copy_constructible<__decay_t<_Functor>>::value,
      |                                                           ^~~~~

Так появилась необходимость в новой функциональной обертке, на этот раз способной работать с некопируемыми типами. И комитет по стандартизации, теперь в 2015-ом, посетил новый пропозал [2]…

A polymorphic wrapper for all Callable objects

Он предлагал ввести unique_function — вариант function, но поддерживающий некопируемые типы. Дело шло без спешки и в итоге этот пропозал в его конечной редакции [3] включили только в C++23 под названием move_only_function. И, мало того, что он позволял работать с некопируемыми типами, в нем были исправлены обнаруженные за прошедшее десятилетие проблемы function:

  1. Беды с const-корректностью

  2. Отсутствие поддержки cv/ref/noexcept-квалифицированных типов

Рассмотрим каждую из них поподробнее на примерах

Беды с const-корректностью

Пример доступен на godbolt

#include 

int main() {
    // Допустим, у нас есть function объект, инициализированный
    // мутабельной лямбдой:
    std::function func{[&]() mutable {
    	// ...
    }};
    // Мы можем его вызывать и всё хорошо:
    func();

    // Но если нам захочется создать константную ссылку на него,
    // рассчитывая, что у нас не получится вызвать function,
    // если объект, захваченный в него, является изменяемым
    const auto &ref{func};
    // То мы не достигнем своих целей! Код ниже скомпилируется!
    // const-не const — для function это ничего не значит
    ref();
}

Отсутствие поддержки cv/ref/noexcept-квалифицированных типов

Пример доступен на godbolt

#include 

void f() noexcept { }

int main() {
  	// — Что-что? Сохранить noexcept спецификатор?
  	// Нет, нас дизайнили в 2002 году! Получай ошибку компиляции!
    std::function fnp = f;
  
  	// — Ну давай хотя бы const?
  	// — Отвали. Compiler Error
    std::function fcp = []() const {};
  
  // ...
}

И все бы хорошо, но…

Copyable function

«Подождите! — воскликнул внимательный читатель, — вот эти все косяки, а почему бы их не исправить в обычном function, не вечность же теперь жить с ними, имея один move_only_function, где они исправлены, и не имея копируемый аналог move_only_function?

Комитет подумал и решил: «В этих словах есть смысл, но мы не готовы исправить сам function. Мы не готовы нарушить устои и традиции, на которые с 2011-го года полагаются тысячи программистов»

Внимательный читатель кивнул, удалясь для дум прочь. Но вскоре вернулся с пропозалом, предлагавшим ввести новый класс, вариант move_only_function, но поддерживающий копируемые типы. Так в C++26 приняли copyable_function [4]

Первый пример

auto lambda{[&]() /*const*/ { … }};

copyable_function func0{lambda};
const auto & ref0{func0};

// Все хорошо, мы вызываем
// неконстантную (по переданной сигнатуре) функцию
// через неконстантный объект
func0();
// Compilation error: пытаемся вызвать
// неконстантную (по переданной сигнатуре) функцию
// через константный объект
ref0(); // operator() is NOT const!

// ---

copyable_function func1{lambda};
const auto & ref1{func1};

// All is OK. Вызываем константную функцию
// 1) Через неконстантный объект
func1();
// 2) Через константный объект
ref1(); // operator() is const!

Второй пример

auto lambda{[&]() mutable { … }};

copyable_function func{lambda};
const auto & ref{func};

// Все хорошо: вызываем
// неконстантную функцию через неконстантный объект
func();
// Compilation error: пытаемся вызвать
// неконстантную функцию через константный объект
ref();

// Compilation error: не можем
// сохранить неконстантную функцию как константную
copyable_function tmp{lambda};

Теперь-то точно все должны быть довольны? :)

Не скажите! Что мы упустили — и function, и move_only_function, и copyable_function — все они по своей семантике владеют завернутыми в них функциями. А что если, если все, что нам нужно — это невладеющий reference на callable объект? Который, между прочим, может быть даже ни копируемым, ни перемещаемым.

Non-owning reference to a Callable

Встречайте function_ref, принятый в C++26 [5]!

Теперь мы можем вызывать то, что нельзя ни копировать, ни перемещать:

#include 

struct A {
    A() { }
    A(const A&) = delete;
    A(A&&) = delete;
    void operator()() { }
};

int main() {
    A obj;

    // Compile errors:
    std::function func1 = obj;
    std::move_only_function func2 = std::move(obj);
    std::copyable_function func3 = obj;

    // All is OK:
    std::function_ref func4 = obj;
    func4();
}

И, вспоминая о колбэках, послуживших одним из ведущих юзкейсов, демонстрирующих полезность function. А разве функция, принимающая колбэк, желает владеть им? Желает оверхед в виде аллокаций? В большинстве случаев — нет.

Так что, на самом деле, это больше юзкейс для function_ref:

data retry(size_t times, function_ref action) {
	// ...
}

Теперь то нам функциональных оберток хватит? Или, может, самое время написать пропозал, предлагающий какую-нибудь еще? :)

Опубликовано при поддержке C++ Moscow

© Habrahabr.ru