2017-10-26 3 views
1

私は組み込みプロジェクトのためのC++クラスライブラリを構築しています。 私が持っている概念の1つはTaskという名前のクラスです。 タスクには2つのメンバーがあります。 整数IDとコールバック関数ポインタC++引数を引数としないコールバックを呼び出す - より良い解決策

コールバックはユーザーによって作成され、クラスのコンストラクターに渡されます。

私は、ユーザーに次のオプションを与えたい:

  • ユーザがタイプvoid (*)(unsigned int)の機能を使用することができますタイプvoid(*)(void)
  • の機能を使用することができます。そのパラメータは、コールバックが呼び出されたときのタスクのIDでなければなりません。
  • ユーザは、タスクはまた、コールバック関数を呼び出すexecute()メソッドを持って

異なるタスクのために上記の関数の種類の組み合わせを使用することができます。このメソッドは、必要に応じて、タスクIDを引数として提供する必要があります。

タスクに 'has_arguments'という名前のbooleanメンバーを追加して、それが設定されていた関数ポインタのタイプコードで与えられています。 また、コールバックメンバのために共用体を使うこともできますし、実行時にvoidポインタを使って適切なものにキャストすることもできます。 タスクの実行には、 'has_arguments'メンバーをチェックし、ポインタを適切な型にキャストし、コールバックを正しい方法で呼び出します。

しかし、この余分なメンバーとチェックを避ける方法はありますか?

void(*)(unsigned int)への関数ポインタを常にキャストしようとしましたが、呼び出すときに常に引数を指定しようとしました。そしてそれは働いた。

引数を引数とする関数を呼び出すのは悪いですか? それは私のために働いたが、それは本当に悪い習慣であると思う。 代わりに私が実装できるものは何ですか?

私は、可変引数リストを持つ関数を作るためにユーザに指示したくありません。私はそれが悪いが、引数で引数を取らない関数を呼び出すことで、彼らに簡単なタスクに依存しない関数を使用する自由を残し またはそのタスクID

+1

暖かいC言語の代わりに 'C++'を実際にコーディングしているのであれば、プレーン関数ポインタの代わりに 'std :: function'を使いたいと思っています。 –

+1

@SamVarshavchikこれは組み込みシステムのためのものではありません。 – Justin

+3

タスクが常に「id」を持っている場合、あなたはいつもそれをユーザに返すのですか? 2つの異なるタイプのコールバックをまったく許可するのはなぜですか?パラメータに 'int'値を渡すのは簡単ではなく、ユーザが望むならば無視することができます。しかし、識別のために、常にそれを渡すべきです。もちろん、void型を間違った型にキャストし、コールバックが期待していない余分なパラメータを渡すと、使用する呼び出し規約に応じて問題が発生する可能性があります。 'stdcall'では大したことですが、' cdecl'では大したことではありません。 –

答えて

2

を認識しているよりインテリジェントな機能を作りたいですか?

もちろんです。

それは私のために働いたが、私はそれが本当に悪い習慣

は、「それは私のために働いた」されていると、プログラムが未定義の動作をした場合に非常に有効な引数ではありません。それは私にとってはうまくいかないかもしれないからです。

代わりにどのように実装することができますか?

私の考えでは、このようなことをタグ付きのユニオンで実装するのが最も効果的です。可能であれば、より安全なタグ付きユニオンには常にboot::variantまたはstd::variantを使用できます。

実際には、余分なメンバーが1人(bool)であるため、パフォーマンスを損なうことはありません。まず、ブランチが実際にボトルネックであることがわかったら、常に最適化することができます。 :)

1

あなたが原因組み込みプラットフォームの使用unionの制限やタグにstd::functionを使用できない場合は、適切に使用し、オーバーロードセッターまたはCTORにより、コールにtypeを設定switchtype上:呼び出し

struct Task { 
    enum CallbackType { 
     cbVoid, 
     cbId 
    }; 
    using VoidCb = void(*)(void); 
    using IdCb = void(*)(unsigned); 

    int   id = 0; 
    CallbackType type; 
    union { 
     VoidCb vcb; 
     IdCb idcb; 
    } function; 

    Task(VoidCb cb) : type(cbVoid), function(cb) {} 
    Task(IdCb cb) : type(cbId), function(cb) {} 

    void call() { 
     switch(type) { 
      case cbVoid : function.vcb(); return; 
      case cbId : function.idcb(id); return; 
     } 
    } 
}; 
1

他の型にキャストされた関数ポインタを介した関数は、未定義の動作を呼び出す(関数ポインタのキャストに関するthins online C++ standard draft参照)。

関数ポインタは、異なるタイプの関数ポインタ に明示的に変換できます。 関数の定義で使用された 型と同じではない関数型([dcl.fct])へのポインタを介して関数を呼び出すことの効果は未定義です。「T2へのポインタ」型(T1とT2は関数型)へのポインタ「T1へのポインタ」型のprvalueを元の型の に変換する以外は、 は元のポインタ値、そのようなポインタ 変換が指定されていません。 [注:ポインタ変換の詳細については の詳細については[conv.ptr]も参照してください。 - 最後のメモ]

したがって、あなたの質問に記載されているように使用しないでください。

この問題を克服する方法は、正しい関数ポインタが1つの場合と他の場合のために格納されるようにすることです。 @Slavaの答えに記述されているように、discriminator/typeデータメンバと2つの別個の関数ポインタメンバ(またはこれらの2つの別個の関数ポインタの和集合)を使って行うことができます。

別のアプローチは、異なるタスクタイプを区別し、継承とオーバーライドを使用してコールバックを正しく実行することです。もちろん、これはあなたのタスクを作成/使用する方法に影響します。しかし、多分型のメンバーや組合よりも拡張性の高い方法で問題を解決することができます。

typedef void(*VoidCallbackType)(void) ; 
typedef void(*IdCallbackType)(unsigned) ; 

struct Task { 

    virtual void doCallback() = 0; 
}; 

struct TaskWithId : Task { 

    unsigned id; 
    IdCallbackType cb; 

    TaskWithId(unsigned id, IdCallbackType cb) : id(id), cb(cb) { } 
    virtual void doCallback() { 
     cb(id); 
    } 
}; 

struct TaskWithoutId: Task { 

    VoidCallbackType cb; 

    TaskWithoutId(VoidCallbackType cb) : cb(cb) { } 
    virtual void doCallback() { 
     cb(); 
    } 
}; 
0

同じインターフェースのように見える表紙の下に何かを作成できます。つまり、コールバックがそれを望まない場合、タスクIDを捨てるヘルパーラッパー関数を自動作成します。

class Task { 

    template <void (*CB)(void)> struct task_void { 
     static void cb (int) { CB(); } 
     operator Task() const { return Task(*this); } 
    }; 

    template <void (*CB)(int)> struct task_int { 
     static void cb (int id) { CB(id); } 
     operator Task() const { return Task(*this); } 
    }; 

    static volatile int next_id_; 

    int id_; 
    void (*cb_)(int); 

    template <typename CB> Task (CB) : id_(next_id_++), cb_(CB::cb) {} 

public: 
    template <void (*CB)(void)> static Task make() { return task_void<CB>(); } 
    template <void (*CB)(int)> static Task make() { return task_int<CB>(); } 
    void execute() { cb_(id_); } 
}; 

プライベートコンストラクタは、初期化のためにヘルパーテンプレートからアクセスできます。ヘルパーテンプレートは、パラメータを非表示にするかどうかを選択できます。

これで、どちらの種類のコールバックでもタスクを作成できます。

void cb_void() { 
    std::cout << __func__ << std::endl; 
} 

void cb_int (int id) { 
    std::cout << __func__ << ':' << id << std::endl; 
} 

//... 
    Task t1 = Task::make<cb_void>(); 
    Task t2 = Task::make<cb_int>(); 
    t1.execute(); 
    t2.execute(); 
+0

こんにちは、私はあなたの答えを理解しようとしています。 タスクオブジェクト 't'を持っていて、t.execute()を実行すると、オブジェクト作成時に2つのコンストラクタのどれが呼び出されたかによって、正しいタイプのコールバックが実行されます。 – user8839561

+0

また、ポインタ 'Task * task_ptr'を作成し、特定のTaskオブジェクト 't'を指し示してから 'task_ptr-> execute()'を実行すると、呼び出されるコールバックは't'の作成中に2つのコンストラクタが使用されましたか? 私はそれを理解するのが難しいです – user8839561

+0

'task_void'と' task_int'は 'Task'sコンストラクタの引数を定義するためのヘルパーテンプレートクラスです。各ヘルパーには、コールバックラッパーと変換演算子があり、 '演算子'コンストラクターに渡します。 'make'メソッドは、右のヘルパーにディスパッチし、それを' Task'に変換することによって 'Task'を構築します。ポインタが必要な場合は、別のビルダーを定義する必要があります。 – jxh

関連する問題