risorse | delegates

Delegates

Introduzione

Quel che segue è la cronaca di un tentativo di implementare i delegate in C++11. Con delegate intendo degli oggetti in grado di “catturare” una funzionalità e di attivarla in un secondo tempo, specificando nel sito di chiamata i parametri sui quali operare. L'intenzione è di riuscire a catturare qualunque oggetto callable, nella fattispecie:

L'infrastruttura è stata sviluppata con il compilatore GCC 4.8.2 su Xubuntu 14.04 e MinGW 4.8.2 su Windows 7 x64, utilizzando su entrambe le piattaforme le opzioni -Wall -Wpedantic -std=c++11.

Implementazione

Ecco alcuni esempi di funzionalità che si intende catturare:

// a free function
int add(int a, int b) {
  return a + b;
}

// a function member
struct Math {
  int add(int a, int b) {
    return a + b;
  }
};

// a function object
struct AddOp {
  int operator()(int a, int b) {
    return a + b;
  }
};

Procediamo per gradi, affrontando inizialmente le funzioni semplici.

Funzioni (e metodi statici)

La funzionalità “dormiente” è conservata all'interno di un oggetto std::function, per la sua generalità (è infatti in grado di gestire sia funzioni semplici, sia funzioni lambda); sempre per generalità, questa volta rispetto al tipo degli argomenti del tipo del valore di ritorno della funzione originale, l'oggetto Delegate, che ha la responsabilità di catturare la funzionalità e di attivarla al momento giusto, fa uso di variadic templates (R rappresenta il tipo del valore di ritorno della funzione, Args l'elenco dei tipi degli argomenti):

#include <cassert>
#include <functional>

int add(int a, int b) {
  return a + b;
}

struct Math {
  static int add(int a, int b) {
    return a + b;
  }
};

template<typename R, typename... Args>
class Delegate {
  typedef std::function<R(Args...)> Fn; // function signature
  Fn fn_;                               // function container
public:
  Delegate(Fn fn) : fn_(fn) { }
  R operator()(Args... args) const {    // function activation
    return fn_(args...);
  }
};

int main() {

  // wrapping a free function
  auto addFn = Delegate<int, int, int>(add);
  assert(addFn(1, 2) == 3);

  // wrapping a static function member
  auto staticAddFnMem = Delegate<int, int, int>(Math::add);
  assert(staticAddFnMem(1, 2) == 3);

  // bonus: delegates are copiable!
  auto copyOfAddFn = addFn;
  assert(copyOfAddFn(1, 2) == 3);

  return 0;
}

Metodi (funzioni membro)

Per attivare una funzione membro serve anche un riferimento all'istanza dell'oggetto sul quale invocare il metodo, che dovrà ovviamente risultare valida all'atto della chiamata — responsabilità demandata al programmatore: un nuovo oggetto di supporto, FnMemCall, si occupa di conservare i riferimenti necessari e di attivare il metodo designato sull'oggetto corretto:

template<class T>
struct FnMemCall {
  T* obj_;                     // instance object
  typedef R (T::*Fn)(Args...); // function member signature
  Fn fn_;                      // function member reference

  // store object instance and function member for later
  FnMemCall(T* obj, Fn fn) : obj_(obj), fn_(fn) { }

  // invoke the function member on the given instance
  R operator()(Args... args) const {
    return (obj_->*fn_)(args...);
  }
};

Un nuovo costruttore di Delegate istanzia l'FnMemCall ausiliario e lo pone in fn_, alla stregua di quanto già accade per le funzioni semplici:

// ...

struct Math {
  static int add(int a, int b) {
    return a + b;
  }
  int diff(int a, int b) {
    return a - b;
  }
};

// ...

template<typename R, typename... Args>
class Delegate {
  typedef std::function<R(Args...)> Fn;
  Fn fn_;
  template<class T>
  struct FnMemCall {
    T* obj_;
    typedef R (T::*Fn)(Args...);
    Fn fn_;
    FnMemCall(T* obj, Fn fn) : obj_(obj), fn_(fn) { }
    R operator()(Args... args) const {
      return (obj_->*fn_)(args...);
    }
  };
public:
  Delegate(Fn fn) : fn_(fn) { }
  template<class T>
  Delegate(T* obj, R (T::*fn)(Args...)) : fn_(FnMemCall<T>(obj, fn)) { }
  R operator()(Args... args) const {
    return fn_(args...);
  }
};

int main() {
  // ...

  // wrapping a non-static function member
  Math math;
  auto diffFnMem = Delegate<int, int, int>(&math, &Math::diff);
  assert(diffFnMem(3, 2) == 1);

  return 0;
}

Oggetti funzione e funzioni lambda

Gli oggetti funzione possono essere trasformati in delegate applicando la tecnica dei metodi all'operatore function-call operator(), ma la forma risulta piuttosto prolissa:

// ...

struct AddOp {
  int operator()(int a, int b) {
    return a + b;
  }
};

int main() {
  // ...

  // wrapping a function object /1
  AddOp addOp;
  auto addOpAsFnMem = Delegate<int, int, int>(&addOp, &AddOp::operator());
  assert(addOpAsFnMem(1, 2) == 3);

  return 0;
};

Grazie all'uso di un oggetto std::function, gli oggetti funzione possono essere catturati con una forma più compatta:

int main() {
  // ...

  // wrapping a function object /2
  auto addOpAsFnObj = Delegate<int, int, int>(addOp);
  assert(addOpAsFnObj(1, 2) == 3);

  return 0;
};

Questa tecnica può essere applicata anche alle funzioni lambda, per la loro affinità con gli oggetti funzione (essendo implementate come istanze di oggetti funzione anonimi con function-call operator const):

int main() {
  // ...

  // wrapping a lambda
  auto add_ = [](int a, int b) { return a + b; };
  auto lambda = Delegate<int, int, int>(add_);
  assert(lambda(1, 2) == 3);

  // wrapping a temporary lambda
  auto lambda2 = Delegate<int, int, int>([](int a, int b) { return a + b; });
  assert(lambda2(1, 2) == 3);

  return 0;
};

Zucchero sintattico

Bello sarebbe poter evitare di specificare i tipi in gioco:

int main() {
  // ...

  auto addFn = Delegate(add);
  auto staticAddFnMem = Delegate(Math::add);
  auto diffFnMem = Delegate(&math, &Math::diff);
  auto addOpAsFnMem = Delegate(&addOp, &AddOp::operator());
  auto addOpAsFnObj = Delegate(addOp);
  auto lambda = Delegate(add_);
  auto lambda2 = Delegate([](int a, int b) { return a + b; });
  // ...

  return 0;
}

Poiché il template argument deduction non si applica alle classi template, ma solo alle funzioni, per elimiare la specifica dei tipi bisogna ricorrere ad una funzione template ausiliaria; per esempio, per attivare il template argument deduction per le funzioni e i metodi statici è sufficiente introdurre una nuova funzione make_delegate così definita:

// ...

template<typename R, typename... Args>
Delegate<R, Args...> make_delegate(R(*fn)(Args...)) {
  return Delegate<R, Args...>(fn);
}

int main() {

  // wrapping a free function
  auto addFn = make_delegate(add);
  assert(addFn(1, 2) == 3);

  // wrapping a static function member
  auto staticAddFnMem = make_delegate(Math::add);
  assert(staticAddFnMem(1, 2) == 3);

  return 0;
}

Per i metodi non statici occorre passare a make_delegate un riferimento all'istanza sulla quale invocare il metodo:

// ...

template<class T, typename R, typename... Args>
Delegate<R, Args...> make_delegate(T* obj, R(T::*fn)(Args...)) {
  return Delegate<R, Args...>(obj, fn);
}

int main() {

  // wrapping a free function
  auto addFn = make_delegate(add);
  assert(addFn(1, 2) == 3);

  // wrapping a static function member
  auto staticAddFnMem = make_delegate(Math::add);
  assert(staticAddFnMem(1, 2) == 3);

  // wrapping a non-static function member
  Math math;
  auto diffFnMem = make_delegate(&math, &Math::diff);
  assert(diffFnMem(3, 2) == 1);

  return 0;
}

Per le lambda e i function object il problema consiste nel recuperare la signature dell'operatore function-call:

// ...

template<typename R, typename... Args>
Delegate<R, Args...> make_delegate(std::function<R(Args...)> fn) {
  return Delegate<R, Args...>(fn);
}

int main() {
  // ...

  // wrapping a function object /2
  AddOp addOp;
  auto addOpAsFnObj = make_delegate(addOp); // compiler error!
  assert(addOpAsFnObj(1, 2) == 3);

  return 0;
}

Il problema è risolvibile in due passi; introducendo dapprima un oggetto traits che ricava la signature dell'operatore function-call; in secondo luogo, definendo una funzione apposita, to_function, che effettua un cast esplicito dell'oggetto funzione a std::function, che il codice fin qui sviluppato è già in grado di trattare:

// ...

template<class F>
struct function_traits;

// function signature capture
template<class R, class... Args>
struct function_traits<R(Args...)> {
  typedef R (*signature)(Args...);
};

// function-call operator signature capture
template <typename FnObj>
struct function_traits : public function_traits<decltype(&FnObj::operator())> {
};

// non-static method signature capture
template <typename T, typename R, typename... Args>
struct function_traits<R(T::*)(Args...)> {
  typedef std::function<R(Args...)> signature;
};

template <typename FnObj>
typename function_traits<FnObj>::signature to_function (FnObj fnObj) {
  return static_cast<typename function_traits<FnObj>::signature>(fnObj);
}

int main() {
  // ...

  // wrapping a function object /2
  AddOp addOp;
  auto addOpAsFnObj = make_delegate(to_function(addOp));
  assert(addOpAsFnObj(1, 2) == 3);

  return 0;
}

to_function prende fnObj per valore anziché per referenza perché altrimenti le lambda passate per r-value reference non sarebbero supportate.

La soluzione è vicina; le lambda tuttavia non sono ancora convertibili a delegate, nemmeno attraverso l'applicazione di to_function, perché l'infrastruttura non supporta gli operator() const:

// ...

int main() {
  // ...

  // wrapping a lambda
  auto add_ = [](int a, int b) { return a + b; };
  auto lambda = make_delegate(to_function(add_)); // compiler error!
  assert(lambda(1, 2) == 3);

  return 0;
}

Una nuova versione di make_delegate che accetta un puntatore a un metodo const risolve la questione:

// ...

template <typename T, typename R, typename... Args>
struct function_traits<R(T::*)(Args...) const> {
  typedef std::function<R(Args...)> signature;
};

int main() {
  // ...

  // wrapping a lambda
  auto add_ = [](int a, int b) { return a + b; };
  auto lambda = make_delegate(to_function(add_));
  assert(lambda(1, 2) == 3);

  return 0;
}

Il problema del const viene così risolto sull'operatore function call, ma permane sugli altri metodi:

// ...

struct Math {
  static int add(int a, int b) {
    return a + b;
  }
  int diff(int a, int b) {
    return a - b;
  }
  int cdiff(int a, int b) const {
    return a - b;
  }
};

struct ConstAddOp {
  int operator()(int a, int b) const {
    return a + b;
  }
};

int main() {
  // ...

  // wrapping a const function object
  ConstAddOp constAddOp;
  auto constAddOpAsFnObj = make_delegate(to_function(constAddOp));
  assert(constAddOpAsFnObj(1, 2) == 3);

  // wrapping a const non-static function member
  auto cdiffFnMem = make_delegate(&math, &Math::cdiff); // compiler error!
  assert(cdiffFnMem(3, 2) == 1);

  return 0;
}

Per rendere compilabile il codice sopra riportato è necessario intervenire in due punti, introducendo una nuova versione const di make_delegate operante sui metodi non statici, oltre che sulla classe Delegate:

// ...

template<typename R, typename... Args>
class Delegate {
  // ...

  template<class T>
  struct ConstFnMemCall {
    const T* obj_;
    typedef R (T::*Fn)(Args...) const;
    Fn fn_;
    ConstFnMemCall(const T* obj, Fn fn) : obj_(obj), fn_(fn) { }
    R operator()(Args... args) const {
      return (obj_->*fn_)(args...);
    }
  };
public:
  // ...

  template<class T>
  Delegate(T* obj, R (T::*fn)(Args...) const) : fn_(ConstFnMemCall<T>(obj, fn)) { }
  // ...

};

template<class T, typename R, typename... Args>
Delegate<R, Args...> make_delegate(T* obj, R(T::*fn)(Args...) const) {
  return Delegate<R, Args...>(obj, fn);
}

È possibile eliminare anche la chiamata esplicita a to_function, inglobandola direttamente in make_delegate, dopo averla modificata in modo da renderla applicabile anche ai puntatori a funzione:

// ...

// function pointer signature capture
template<class R, class... Args>
struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)> {
};

template<class Fn>
auto make_delegate(Fn fn) -> decltype(make_delegate(to_function(fn))) {
  return make_delegate(to_function(fn));
}

int main() {
  auto addFn = make_delegate(add);
  assert(addFn(1, 2) == 3);

  // wrapping a static function member
  auto staticAddFnMem = make_delegate(Math::add);
  assert(staticAddFnMem(1, 2) == 3);

  // wrapping a non-static function member
  Math math;
  auto diffFnMem = make_delegate(&math, &Math::diff);
  assert(diffFnMem(3, 2) == 1);

  // wrapping a function object /2
  AddOp addOp;
  auto addOpAsFnObj = make_delegate(to_function(addOp));
  assert(addOpAsFnObj(1, 2) == 3);

  // wrapping a lambda
  auto add_ = [](int a, int b) { return a + b; };
  auto lambda = make_delegate(to_function(add_));
  assert(lambda(1, 2) == 3);

  // wrapping a temporary lambda
  auto lambda2 = make_delegate(to_function([](int a, int b) { return a + b; }));
  assert(lambda2(1, 2) == 3);

  // wrapping a const function object
  ConstAddOp constAddOp;
  auto constAddOpAsFnObj = make_delegate(to_function(constAddOp));
  assert(constAddOpAsFnObj(1, 2) == 3);

  // wrapping a const non-static function member
  auto cdiffFnMem = make_delegate(&math, &Math::cdiff);
  assert(cdiffFnMem(3, 2) == 1);
}

Limiti

Non ho ancora avuto modo di impiegare i delegate in progetti reali. Durante lo sviluppo, tuttavia, è emersa la necessità di esplicitare i tipi in gioco nel momento in cui si intende catturare una funzione overloaded, cioè in tutti i casi di omonimia:

// ...

int overloaded(double /*x*/) {
  return 0;
}

int overloaded(const std::string& /*s*/) {
  return 1;
}


int main() {
  // ...

  /* won't compile
   * auto overloaded1 = make_delegate(f);
   */

  auto overloaded2 =
    make_delegate(static_cast<int (*)(double)>(overloaded));
  assert(overloaded2(3.14) == 0);

  auto overloaded3 =
    make_delegate(static_cast<int (*)(const std::string&)>(overloaded));
  assert(overloaded3("acme") == 1);

  return 0;
}

Sorgenti

Riferimenti

Pagina modificata il 18/08/2014