前提:
提案する設計は機能しますが、通常の関数ポインターを使用すると、登録できるコールバックの種類が大幅に制限されます。より強力ですが、固定インターフェイスからの継承に基づくアプローチはより冗長であり、クライアントがコールバックを定義します。
この回答では、最初std::function
にこの目的で使用する方法の例をいくつか示します。std::function
例は、あなたが概説した種類のソリューションとは対照的に、使用がどのようにそしてなぜ利点をもたらすかを示して、ほとんどそれ自体を物語っています。
ただし、に基づく素朴なアプローチにstd::function
は、それ自体の制限もあります。これをリストします。これが、最終的にBoost.Signals2を確認することをお勧めする理由です。これは非常に強力で使いやすいライブラリです。この回答の最後でBoost.Signals2について説明します。うまくいけば、最初に基づいた単純な設計を理解するstd::function
ことで、後で信号とスロットのより複雑な側面を理解しやすくなります。
std ::function<>に基づくソリューション
いくつかの簡単なクラスを紹介し、いくつかの具体的な例の土台を準備しましょう。ここで、anorder
は、を持ち、id
いくつかのを含むものitem
です。それぞれitem
はtype
(簡単にするために、ここでは本とDVDのどちらでもかまいません)とname
:で記述されます。
#include <vector>
#include <memory>
#include <string>
struct item // A very simple data structure for modeling order items
{
enum type { book, dvd };
item(type t, std::string const& s) : itemType(t), name(s) { }
type itemType; // The type of the item
std::string name; // The name of the item
};
struct order // An order has an ID and contains a certain number of items
{
order(int id) : id(id) { }
int get_id() const { return id; }
std::vector<item> const& get_items() const { return items; }
void add_item(item::type t, std::string const& n)
{ items.emplace_back(t, n); }
private:
int id;
std::vector<item> items;
};
ここで概説するソリューションの中心は、次のクラスと、クライアントによって登録されたコールバックを保持するためorder_repository
のその内部使用法です。std::function
コールバックは関数を介して登録でき、登録時に返されるCookieを提供することによりregister_callback()
、関数を介して(非常に直感的に)登録を解除できます。unregister_callback()
registered_callback()
place_order()
注文する機能と、すべての注文の処理をトリガーする機能process_order()
があります。これにより、登録されているすべてのハンドラーが順番に呼び出されます。各ハンドラーは、発注された注文の同じベクトルへの参照を受け取ります。
#include <functional>
using order_ptr = std::shared_ptr<order>; // Just a useful type alias
class order_repository // Collects orders and registers processing callbacks
{
public:
typedef std::function<void(std::vector<order_ptr>&)> order_callback;
template<typename F>
size_t register_callback(F&& f)
{ return callbacks.push_back(std::forward<F>(f)); }
void place_order(order_ptr o)
{ orders.push_back(o); }
void process_all_orders()
{ for (auto const& cb : callbacks) { cb(orders); } }
private:
std::vector<order_callback> callbacks;
std::vector<order_ptr> orders;
};
このソリューションの強みは、を使用して型消去をstd::function
実現し、あらゆる種類の呼び出し可能なオブジェクトをカプセル化できるようにすることです。
次のヘルパー関数は、いくつかの注文を生成して発注するために使用し、セットアップを完了します(4つの注文を作成し、各注文にいくつかのアイテムを追加するだけです)。
void generate_and_place_orders(order_repository& r)
{
order_ptr o = std::make_shared<order>(42);
o->add_item(item::book, "TC++PL, 4th Edition");
r.place_order(o);
o = std::make_shared<order>(1729);
o->add_item(item::book, "TC++PL, 4th Edition");
o->add_item(item::book, "C++ Concurrency in Action");
r.place_order(o);
o = std::make_shared<order>(24);
o->add_item(item::dvd, "2001: A Space Odyssey");
r.place_order(o);
o = std::make_shared<order>(9271);
o->add_item(item::dvd, "The Big Lebowski");
o->add_item(item::book, "C++ Concurrency in Action");
o->add_item(item::book, "TC++PL, 4th Edition");
r.place_order(o);
}
次に、提供できるコールバックの種類を見てみましょう。手始めに、すべての注文を出力する通常のコールバック関数を用意しましょう。
void print_all_orders(std::vector<order_ptr>& orders)
{
std::cout << "Printing all the orders:\n=========================\n";
for (auto const& o : orders)
{
std::cout << "\torder #" << o->get_id() << ": " << std::endl;
int cnt = 0;
for (auto const& i : o->get_items())
{
std::cout << "\t\titem #" << ++cnt << ": ("
<< ((i.itemType == item::book) ? "book" : "dvd")
<< ", " << "\"" << i.name << "\")\n";
}
}
std::cout << "=========================\n\n";
}
そしてそれを使用する簡単なプログラム:
int main()
{
order_repository r;
generate_and_place_orders(r);
// Register a regular function as a callback...
r.register_callback(print_all_orders);
// Process the order! (Will invoke all the registered callbacks)
r.process_all_orders();
}
これは、このプログラムの出力を示す実際の例です。
かなり合理的に、通常の関数のみを登録することに限定されません。いくつかの状態情報を保持するファンクターを含め、呼び出し可能なオブジェクトはすべてコールバックとして登録できます。上記の関数を、上記の関数と同じ詳細な注文リストを印刷できるファンクターとして、または注文アイテムを含まない短い要約として書き直してみましょう。print_all_orders()
struct print_all_orders
{
print_all_orders(bool detailed) : printDetails(detailed) { }
void operator () (std::vector<order_ptr>& orders)
{
std::cout << "Printing all the orders:\n=========================\n";
for (auto const& o : orders)
{
std::cout << "\torder #" << o->get_id();
if (printDetails)
{
std::cout << ": " << std::endl;
int cnt = 0;
for (auto const& i : o->get_items())
{
std::cout << "\t\titem #" << ++cnt << ": ("
<< ((i.itemType == item::book) ? "book" : "dvd")
<< ", " << "\"" << i.name << "\")\n";
}
}
else { std::cout << std::endl; }
}
std::cout << "=========================\n\n";
}
private:
bool printDetails;
};
これを小さなテストプログラムで使用する方法は次のとおりです。
int main()
{
using namespace std::placeholders;
order_repository r;
generate_and_place_orders(r);
// Register one particular instance of our functor...
r.register_callback(print_all_orders(false));
// Register another instance of the same functor...
r.register_callback(print_all_orders(true));
r.process_all_orders();
}
そして、これがこのライブ例に示されている対応する出力です。
によって提供される柔軟性のおかげで、結果をコールバックとしてstd::function
登録することもできます。std::bind()
これを例で示すために、さらにクラスを紹介しましょうperson
。
#include <iostream>
struct person
{
person(std::string n) : name(n) { }
void receive_order(order_ptr spOrder)
{ std::cout << name << " received order " << spOrder->get_id() << std::endl; }
private:
std::string name;
};
クラスperson
にはメンバー関数がありますreceive_order()
。特定のオブジェクトを呼び出すreceive_order()
と、特定のオブジェクトがそのオブジェクトに配信されたperson
という事実がモデル化されます。order
person
上記のクラス定義を使用して、すべての注文を1人の人にディスパッチするコールバック関数を登録できます(実行時に決定できます!):
void give_all_orders_to(std::vector<order_ptr>& orders, person& p)
{
std::cout << "Dispatching orders:\n=========================\n";
for (auto const& o : orders) { p.receive_order(o); }
orders.clear();
std::cout << "=========================\n\n";
}
この時点で、2つのコールバックを登録する次のプログラムを作成できます。以前に使用した注文を印刷するための同じ関数と、の特定のインスタンスに注文をディスパッチするための上記の関数ですPerson
。これが私たちのやり方です:
int main()
{
using namespace std::placeholders;
order_repository r;
generate_and_place_orders(r);
person alice("alice");
r.register_callback(print_all_orders);
// Register the result of binding a function's argument...
r.register_callback(std::bind(give_all_orders_to, _1, std::ref(alice)));
r.process_all_orders();
}
このプログラムの出力は、このライブの例に示されています。
そしてもちろん、コールバックとしてラムダを使用することもできます。次のプログラムは、前のプログラムに基づいて構築されており、ある人に小さな注文をディスパッチし、別の人に大きな注文をディスパッチするラムダコールバックの使用法を示しています。
int main()
{
order_repository r;
generate_and_place_orders(r);
person alice("alice");
person bob("bob");
r.register_callback(print_all_orders);
r.register_callback([&] (std::vector<order_ptr>& orders)
{
for (auto const& o : orders)
{
if (o->get_items().size() < 2) { bob.receive_order(o); }
else { alice.receive_order(o); }
}
orders.clear();
});
r.process_all_orders();
}
繰り返しになりますが、このライブの例は対応する出力を示しています。
std :: function <>(Boost.Signals2)を超えて
上記のデザインは比較的シンプルで、非常に柔軟性があり、使いやすいです。ただし、許可されていないことがたくさんあります。
- 特定のコールバックへのイベントのディスパッチを簡単にフリーズして再開することはできません。
- 関連するコールバックのセットをイベント
クラスにカプセル化しません。
- コールバックをグループ化して順序付けることはできません。
- コールバックが値を返すことはできません。
- これらの戻り値を組み合わせることはできません。
これらのすべての機能は、他の多くの機能とともに、 Boost.Signals2などの本格的なライブラリによって提供されます。上記の設計に精通していると、それがどのように機能するかを理解しやすくなります。
たとえば、これは、シグナルを定義し、2つの単純なコールバックを登録し、シグナルの呼び出し演算子を呼び出すことによって両方を呼び出す方法です(リンクされたドキュメントページから)。
struct Hello
{
void operator()() const
{
std::cout << "Hello";
}
};
struct World
{
void operator()() const
{
std::cout << ", World!" << std::endl;
}
};
int main()
{
boost::signals2::signal<void ()> sig;
sig.connect(Hello());
sig.connect(World());
sig();
}
いつものように、これは上記のプログラムの実例です。