Альтернатива для обратных вызовов с использованием std::function

В настоящее время я пробую код, который делает следующее:

void f(int x) { cout << "f("<<x<<")" << endl; }

class C
{
public:
   void m(int x) { cout << "C::m("<<x<<")" << endl; }
};

class C2
{
public:
   void registerCallback(function<void(int)> f)
   {
      v.push_back(f);
   }

private:
   vector<function<void(int)>> v;

   void callThem()
   {
      for (int i=0; i<v.size(); i++)
      {
         v[i](i);
      }
   }
};

int main()
{
   C2 registrar;

   C c;
   registrar.registerCallback(&f); // Static function
   registrar.registerCallback(bind(&C::m, &c, placeholders::_1)); // Method

   return 0;
}

Это работает очень хорошо. Однако я застрял с этим шаблоном. Я хотел бы проверить, был ли уже зарегистрирован обратный вызов, и я хотел бы иметь возможность отменить регистрацию обратного вызова, удалив его из вектора. Я только что узнал, что std::function объекты нельзя сравнивать, а значит, невозможно искать их существование в контейнере.

Поэтому мне нужна альтернатива. Конечно, хотелось бы сохранить проверки типов во время компиляции и возможность регистрировать методы произвольных классов.

Как я могу добиться аналогичного решения, позволяющего отменить регистрацию обратного вызова и проверить двойную регистрацию? Есть ли компромиссы, на которые я должен пойти?


person Silicomancer    schedule 23.12.2014    source источник
comment
Это может помочь. Реализованный там класс делегата функции-члена делает гораздо больше, чем вы просите, но, в частности, в нем реализовано упорядочение.   -  person Pradhan    schedule 23.12.2014


Ответы (3)


Основная проблема заключается в том, что большинство функциональных объектов несопоставимы. В то время как простые указатели функций и пользовательские объекты функций с оператором равенства можно сравнивать, лямбда-выражения, результат std::bind() и т. д. не могут. Использование адреса функциональных объектов для их идентификации, как правило, не является подходящим подходом, поскольку объекты имеют тенденцию к копированию. Возможно можно использовать std::reference_wrapper<Fun>, чтобы избежать их копирования, но объекты, хранящиеся в std::function<F>, по-прежнему будут иметь другой адрес.

С вариативными шаблонами C++11 достаточно легко создать пользовательскую версию std::function<...>, которая предоставляет средства сравнения. Их может быть даже две версии:

  • Одна версия, которая принимает произвольные функциональные объекты, но, очевидно, может сравнивать только сравнимые функциональные объекты: в зависимости от того, предоставляет ли аргумент конструктора оператор равенства или нет, используется подходящий базовый класс.
  • Одна версия, которая всегда обеспечивает рабочее сравнение и, очевидно, не может использоваться с объектами, не сравнимыми по равенству.

Последнее определить немного проще, и оно будет выглядеть примерно так:

template <typename...> class comparable_function;
template <typename RC, typename... Args>
class comparable_function<RC(Args...)> {
    struct base {
        virtual ~base() {}
        virtual RC    call(Args... args) = 0;
        virtual base* clone() const = 0;
        virtual bool  compare(base const*) const = 0;
    };
    template <typename Fun>
    struct concrete: base {
        Fun fun;
        concrete(Fun const& fun): fun(fun) {}
        RC call(Args... args) { return this->fun(args...); }
        base* clone() const { return new concrete<Fun>(this->fun); }
        bool compare(base const* other) const {
             concrete const* o = dynamic_cast<concrete<Fun>>(other);
             return o && this->fun == o->fun;
        }
    };
    std::unique_ptr<base> fun;
public:
    template <typename Fun>
    comparable_function(Fun fun): fun(new concrete<Fun>(fun)) {}
    comparable_function(comparable_function const& other): fun(other.fun->clone()) {}
    RC operator()(Args... args) { return this->fun->call(args); }
    bool operator== (comparable_function const& other) const {
        return this->fun->compare(other.fun.get());
    }
    bool operator!= (comparable_function const& other) { return !(this == other); }
};

Наверное, я что-то забыл (и/или опечатался), но это то, что нужно. Для необязательно сопоставимой версии у вас будет две версии concrete: одна, которая реализована, как указано выше, и другая, которая всегда возвращает false. В зависимости от того, есть ли в конструкторе оператор == для Fun, вы должны создать тот или иной.

person Dietmar Kühl    schedule 23.12.2014
comment
Это очень интересно. Но я не уверен, что это действительно решает мою проблему. В основном мне нужно зарегистрировать методы как обратные вызовы. Итак, AFAIK, я вынужден использовать bind, который, как вы сказали, несопоставим. Ваш подход не может быть использован в этом случае, не так ли? - person Silicomancer; 24.12.2014
comment
Ну, вас точно не заставляют использовать std::bind()! Вместо этого вы можете создать версию std::bind(), которая принимает только сравнимые объекты функций (указатели на члены сравнимы) и сравнимые параметры (вы, вероятно, захотите сравнить и их) и предоставляет оператор сравнения. В зависимости от ваших потребностей вы можете избежать создания ограниченной версии std::bind(): хотя я думаю, что примерно знаю, как реализовать std::bind(), я этого не делал, и я почти уверен, что это потребует больше кода, чем я готов напечатать в ответ на вопрос SO. - person Dietmar Kühl; 24.12.2014
comment
Я понимаю. Спасибо, в любом случае! Знаете ли вы, есть ли какой-нибудь проверенный код, реализующий этот подход? Я полагаю, что я не первый, кто пытается решить эту проблему. - person Silicomancer; 24.12.2014
comment
Вы можете проверить Boost, чтобы узнать, внедрили ли они аналогичную версию. Я могу попытаться создать версию позже сегодня (я все еще думаю, что реализация std::bind() не так сложна, но мне нужно сделать это, чтобы выяснить это), но, конечно, это не будет квалифицироваться как < я>хорошо старался :-) - person Dietmar Kühl; 24.12.2014
comment
Отлично сюда. У меня нет опыта работы с этой частью стандарта, поэтому мне, вероятно, потребуется много времени, чтобы реализовать что-то подобное. - person Silicomancer; 24.12.2014
comment
Кажется, что указатели на функции повышения существуют и их можно сравнивать, но они не поддерживают привязку. - person Silicomancer; 24.12.2014
comment
Я поместил первоначальную реализацию bind() с объектами функций, поддерживающими сравнение, в ветвь моей работы на стандартная библиотека C++ (см. файлы в src/nstd/functional). Все сравнения работают только при попытке сравнить идентичные типы, т.е. их должно быть достаточно для использования в comparable_function. Однако в настоящее время отсутствует поддержка reference_wrapper и вызовов, подобных указателям, которые необходимы для вызовов существующих объектов. - person Dietmar Kühl; 24.12.2014

Ну, а что, если вы сделаете что-то вроде:

class C2
{
public:
   void registerCallback(function<void(int)>* f)
   {
      v.push_back(f);
   }

private:
   vector<function<void(int)>*> v;

   void callThem()
   {
      for (int i=0; i<v.size(); i++)
      {
         v[i][0](i);
      }
   }
};

Функции несопоставимы, но указатели сравнимы. Причина, по которой функции несопоставимы, заключается в том, что нет способа определить, равны ли функции (не только в C++, в информатике). То есть невозможно определить, имеют ли функции одинаковое значение. Однако, используя указатели, мы можем, по крайней мере, увидеть, занимают ли они одно и то же место в памяти.

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

person Matt G    schedule 23.12.2014
comment
Интересно ! Вы даже можете передать f по ссылке (нет необходимости изменять внешний код) и взять его адрес при регистрации! - person Christophe; 23.12.2014
comment
И, возможно, использовать набор вместо вектора, чтобы предотвратить двойную регистрацию. Это разрешено снова, потому что указатели сопоставимы. - person Alex; 23.12.2014
comment
Если я правильно понимаю ваш подход, мне нужно будет использовать точно такой же объект функции для регистрации и отмены регистрации, верно? Но как это может решить проблему двойной регистрации? Конечно, нельзя использовать один и тот же объект функции для случайной регистрации одной и той же функции дважды. - person Silicomancer; 24.12.2014

Я думаю, что есть фундаментальная проблема с автоматическим обнаружением двойной регистрации. Когда вы считаете две функции идентичными? Для обычных указателей на функции вы можете использовать адрес, но с std::bind и особенно с лямбда-функциями у вас возникнут проблемы:

class C2
{
public:
   void registerCallback(??? f)
   {
      if (v.find(f, ???) == v.end())
          v.push_back(f);
   }
private:
   vector<function<void(int)>> v;
};

void f1(int);
void f3(int, int);
void f2(int)
{
   C2 c;
   c.registerCallback(f1);
   c.registerCallback(f1); // could be detected by address
   c.registerCallback([](int x) {});
   c.registerCallback([](int x) {}); // detected ?!? 
   c.registerCallback(std::bind(f3, _1, 1);
   c.registerCallback([](int x) {f3(x,1);}) ; // detected? 
}

Компилятор не может определить, что две лямбда-функции семантически идентичны.

Я бы изменил register, чтобы он возвращал идентификатор (или объект подключения, как в Boost.Signal2), который может использоваться клиентами для отмены регистрации обратных вызовов. Однако это не предотвратит двойную регистрацию.

class C2
{
public:
   typedef ??? ID;
   ID registerCallback(??? f)
   {
      ?? id = new_id();
      v[id] = f;
   }
private:
   map<???, function<void(int)>> v;
};
person Jens    schedule 23.12.2014
comment
Я согласен. Лямбды нельзя сравнивать. Поэтому к ним нужно относиться более элементарно. Но все же наиболее важными вариантами использования являются методы и статические функции. Оба можно сравнивать. Так что может быть решение, позволяющее двойную проверку регистрации и простую отмену регистрации для этих двух случаев, верно? - person Silicomancer; 24.12.2014
comment
@Silicomancer Мой опыт показывает, что я почти всегда регистрирую лямбда-функции (или связанные функции) в качестве обратных вызовов, потому что большую часть времени я хочу зарегистрировать функции-члены некоторых объектов. Там мне нужно привязать указатель this. - person Jens; 24.12.2014