14

ゴール:

無関係な型、つまり共通の基本クラスを持たない型で、型セーフな動的ポリモーフィズム(つまり、関数呼び出しの実行時ディスパッチ)を実現したいと思います。これは達成可能であるか、少なくとも理論的には健全であるように思われます。私は自分の問題をより正式に定義しようとします。

問題の定義:

次の場合:

  • 2つ以上の無関係なA1, ..., An。それぞれに、と呼ばれるメソッドがfあり、署名が異なる可能性がありますが、戻り型は同じ Rです。と
  • boost::variant<A1*, ..., An*>オブジェクト(または他のvタイプのバリアント)。これらのタイプのいずれかの値をいつでも想定でき、想定する必要があります。

私の目標は、に含まれる値の実際のタイプがである場合に機能するように実行時にディスパッチさv.f(arg_1, ..., arg_m);れるものと概念的に同等の命令を作成することです。呼び出し引数が各関数の仮パラメーターと互換性がない場合、コンパイラーはエラーを発生させる必要があります。Ai::fvAiAi

もちろん、構文に固執する必要はありません。v.f(arg_1, ..., arg_m)たとえば、のようなものcall(v, f, ...)も受け入れられます。

私はこれをC++で達成しようとしましたが、これまでのところ、良い解決策を思い付くことができませんでした(私にはたくさんの悪い解決策があります)。以下に、「良い解決策」の意味を明確にします。

制約:

良い解決策は、v.f(...)イディオムを模倣できるもの、たとえばcall_on_variant(v, f, ...);次の制約を満たすものです。

  1. この方法で呼び出す必要のある関数ごとに(たとえば)、またはコード内の別の場所、特にグローバルスコープで多態的に処理できる(たとえば)無関係な型のリストに対して、個別の宣言を行う必要はありません。fENABLE_CALL_ON_VARIANT(f)A1, ..., AnENABLE_VARIANT_CALL(A1, ..., An)
  2. 呼び出しを行うときに、入力引数のタイプに明示的に名前を付ける必要はありません(例call_on_variant<int, double, string>(v, f, ...))。リターンタイプの命名はOKなので、たとえばcall_on_variant<void>(v, f, ...)受け入れられます。

うまくいけば私の願いと要件を明確にする実証的な例に従います。

例:

struct A1 { void f(int, double, string) { cout << "A"; } };
struct A2 { void f(int, double, string) { cout << "B"; } };
struct A3 { void f(int, double, string) { cout << "C"; } };

using V = boost::variant<A1, A2, A3>;

// Do not want anything like the following here:
// ENABLE_VARIANT_CALL(foo, <whatever>)

int main()
{
    A a;
    B b;
    C c;

    V v = &a;
    call_on_variant(v, f, 42, 3.14, "hello");

    // Do not want anything like the following here:
    // call_on_variant<int, double, string>(v, f, 42, 3.14, "hello");

    V v = &b;
    call_on_variant(v, f, 42, 3.14, "hello");

    V v = &c;
    call_on_variant(v, f, 42, 3.14, "hello");
}

このプログラムの出力は次のようになりますABC

最良の(失敗した)試み:

目的のソリューションに最も近いのは、次のマクロです。

#define call_on_variant(R, v, f, ...) \
[&] () -> R { \
    struct caller : public boost::static_visitor<void> \
    { \
        template<typename T> \
        R operator () (T* pObj) \
        { \
            pObj->f(__VA_ARGS__); \
        } \
    }; \
    caller c; \
    return v.apply_visitor(c); \
}();

テンプレートメンバーのみがローカルクラスで許可されている場合、これは完全に機能します(この質問を参照)。誰かがこれを修正する方法、または別のアプローチを提案する方法を知っていますか?

4

4 に答える 4

7

時が経ち、C++14 が完成し、コンパイラは汎用ラムダなどの新機能のサポートを追加しています。

ジェネリックラムダは、以下に示す機構とともに、無関係なクラスで目的の (動的) ポリモーフィズムを実現できます。

#include <boost/variant.hpp>

template<typename R, typename F>
class delegating_visitor : public boost::static_visitor<R>
{
public:
    delegating_visitor(F&& f) : _f(std::forward<F>(f)) { }
    template<typename T>
    R operator () (T x) { return _f(x); }
private:
    F _f;
};

template<typename R, typename F>
auto make_visitor(F&& f)
{
    using visitor_type = delegating_visitor<R, std::remove_reference_t<F>>;
    return visitor_type(std::forward<F>(f));
}

template<typename R, typename V, typename F>
auto vcall(V&& vt, F&& f)
{
    auto v = make_visitor<R>(std::forward<F>(f));
    return vt.apply_visitor(v);
}

#define call_on_variant(val, fxn_expr) \
    vcall<int>(val, [] (auto x) { return x-> fxn_expr; });

これを実践してみましょう。次の 2 つの無関係なクラスがあるとします。

#include <iostream>
#include <string>

struct A
{
    int foo(int i, double d, std::string s) const
    { 
        std::cout << "A::foo(" << i << ", " << d << ", " << s << ")"; 
        return 1; 
    }
};

struct B
{
    int foo(int i, double d, std::string s) const
    { 
        std::cout << "B::foo(" << i << ", " << d << ", " << s << ")"; 
        return 2;
    }
};

foo()この方法でポリモーフィックに呼び出すことができます。

int main()
{
    A a;
    B b;

    boost::variant<A*, B*> v = &a;
    auto res1 = call_on_variant(v, foo(42, 3.14, "Hello"));
    std::cout << std::endl<< res1 << std::endl;

    v = &b;
    auto res2 = call_on_variant(v, foo(1337, 6.28, "World"));
    std::cout << std::endl<< res2 << std::endl;
}

出力は、期待どおりです。

A::foo(42, 3.14, Hello)
1
B::foo(1337, 6.28, World)
2

このプログラムは、2013 年 11 月の CTP を使用して VC12 でテストされています。残念ながら、ジェネリック ラムダをサポートするオンライン コンパイラを知らないため、実際の例を投稿することはできません。

于 2013-12-10T20:55:39.020 に答える
4

OK、ここにワイルドショットがあります:

template <typename R, typename ...Args>
struct visitor : boost::static_visitor<R>
{
    template <typename T>
    R operator()(T & x)
    { 
        return tuple_unpack(x, t);   // this needs a bit of code
    }

    visitor(Args const &... args) : t(args...) { }

private:
    std::tuple<Args...> t;
};

template <typename R, typename Var, typename ...Args>
R call_on_variant(Var & var, Args const &... args)
{
    return boost::apply_visitor(visitor<R, Args...>(args...), var);
}

使用法:

R result = call_on_variant<R>(my_var, 12, "Hello", true);

タプルをアンパックして関数を呼び出すために必要な作業量を隠しましたが、これは SO の他の場所で行われたと思います。

また、引数のコピーではなく参照を保存する必要がある場合は、これを行うこともできますが、より注意が必要です。(参照のタプルを持つことができます。ただし、一時オブジェクトも許可するかどうかを検討する必要があります。)

于 2013-01-11T23:59:44.897 に答える
4

残念ながら、これは C++ では実行できません (まだ - 結論を参照してください)。証明に従います。

考慮事項 1: [テンプレートの必要性について]

Ai::f実行時に式call_on_variant(v, f, ...)(またはそれと同等の形式) が一致したときに呼び出される正しいメンバー関数を決定するには、バリアント オブジェクト を指定して、 によって保持されている値vの型を取得する必要があります。これを行うには、必ず少なくとも 1 つの (クラスまたは関数) template の定義が必要です。Aiv

この理由は、これがどのように行われたとしても、バリアントが保持できるすべての型を反復処理する必要があるためです(型リストは として公開されboost::variant<...>::types、バリアントがその型の値を保持しているかどうかを確認します (を介してboost::get<>))。 (そうであれば) その値を、メンバー関数の呼び出しを実行する必要があるポインターとして取得します(内部的には、これも実行されますboost::apply_visitor<>)。

リスト内の単一のタイプごとに、これは次の方法で実行できます。

using types = boost::variant<A1*, ..., An*>::types;
mpl::at_c<types, I>::type* ppObj = (get<mpl::at_c<types, I>::type>(&var));
if (ppObj != NULL)
{
    (*ppObj)->f(...);
}

Iはコンパイル時の定数です。残念ながら、C++では、コンパイル時の for loop に基づいて、このようなスニペットのシーケンスをコンパイラによって生成できるイディオムが許可されていません。代わりに、次のようなテンプレート メタプログラミング手法を使用する必要があります。static for

mpl::for_each<types>(F());

whereFはテンプレート呼び出し演算子を持つファンクターです。直接的または間接的に、少なくとも 1 つのクラスまたは関数テンプレートを定義する必要static forあります。

考慮事項 2: [局所性の必要性について]

目的のソリューションの制約の 1 つ (質問のテキストのセクション「制約」の要件 1 )は、関数呼び出しが行われているスコープ以外のスコープでグローバル宣言またはその他の宣言を追加する必要がないことです。終わり。したがって、マクロ展開またはテンプレート メタ プログラミングが関与するかどうかに関係なく、実行する必要があることは、関数呼び出しが発生する場所で実行する必要があります

上記の「考慮事項 1」で、タスクを実行するには少なくとも 1 つのテンプレートを定義する必要があることが証明されているため、これには問題があります。問題は、C++ではテンプレートをローカル スコープで定義できないことです。これはクラス テンプレートと関数テンプレートに当てはまり、この制限を克服する方法はありません。§14/2:

「テンプレート宣言は、名前空間スコープまたはクラス スコープ宣言としてのみ表示できます」

したがって、ジョブを実行するために定義する必要がある一般的なルーチンは、 call site 以外の場所で定義する必要があり、適切な引数を使用して call-site でインスタンス化する必要があります。

考慮事項 3: [関数名について]

call_on_variant()マクロ (または同等の構造体) は可能なすべての function を処理できなければならないためf、 の名前fテンプレートベースの型解決機構に引数として渡す必要があります。呼び出す必要がある特定の関数はテンプレート機構によって決定される必要があるため、関数の名前のみが渡されることを強調することが重要です。 Ai::f

ただし、名前は型システムに属していないため、テンプレート引数にすることはできません。

結論:

上記の 3 つの考慮事項を組み合わせることで、現時点でこの問題を C++ で解決できないことが証明されています。名前をテンプレート引数として使用できるか、ローカル テンプレートを定義できる必要があります。最初のものは少なくとも望ましくありませんが、2番目のものは理にかなっているかもしれませんが、標準化委員会によって考慮されていません. ただし、1 つの例外が認められる可能性があります

将来の機会:

次の C++ 標準に入るために強くプッシュされているジェネリック ラムダは、実際にはテンプレート呼び出し operator を持つローカル クラスです。

したがって、質問のテキストの最後に投稿したマクロはまだ機能しませんが、別のアプローチが実行可能のようです (戻り値の型を処理するために微調整が必​​要です):

// Helper template for type resolution
template<typename F, typename V>
struct extractor
{
    extractor(F f, V& v) : _f(f), _v(v) { }

    template<typename T>
    void operator () (T pObj)
    {
        T* ppObj = get<T>(&_v));
        if (ppObj != NULL)
        {
            _f(*ppObj);
            return;
        }
    }

    F _f;
    V& _v;
};

// v is an object of type boost::variant<A1*, ..., An*>;
// f is the name of the function to be invoked;
// The remaining arguments are the call arguments.
#define call_on_variant(v, f, ...) \
    using types = decltype(v)::types; \
    auto lam = [&] (auto pObj) \
    { \
        (*pObj)->f(__VA_ARGS__); \
    }; \
    extractor<decltype(lam), decltype(v)>(); \
    mpl::for_each<types>(ex);

最後に:

これは、(悲しいことに) C++ でサポートされていないタイプ セーフな呼び出しの興味深いケースです。Mat Marcus、Jaakko Jarvi、Sean Parent によるこの論文は、無関係な型に対する動的ポリモーフィズムが、プログラミングにおける重要な (私の意見では、基本的で避けられない)パラダイム シフトを達成するために不可欠であることを示しているようです。

于 2013-01-12T11:06:38.490 に答える
0

私はかつて.NETデリゲートをシミュレートすることでこれを解決しました:

template<typename T>
class Delegate
{
    //static_assert(false, "T must be a function type");
};

template<typename ReturnType>
class Delegate<ReturnType()>
{
private:
    class HelperBase
    {
    public:
        HelperBase()
        {
        }

        virtual ~HelperBase()
        {
        }

        virtual ReturnType operator()() const = 0;
        virtual bool operator==(const HelperBase& hb) const = 0;
        virtual HelperBase* Clone() const = 0;
    };

    template<typename Class>
    class Helper : public HelperBase
    {
    private:
        Class* m_pObject;
        ReturnType(Class::*m_pMethod)();

    public:
        Helper(Class* pObject, ReturnType(Class::*pMethod)()) : m_pObject(pObject), m_pMethod(pMethod)
        {
        }

        virtual ~Helper()
        {
        }

        virtual ReturnType operator()() const
        {
            return (m_pObject->*m_pMethod)();
        }

        virtual bool operator==(const HelperBase& hb) const
        {
            const Helper& h = static_cast<const Helper&>(hb);
            return m_pObject == h.m_pObject && m_pMethod == h.m_pMethod;
        }

        virtual HelperBase* Clone() const
        {
            return new Helper(*this);
        }
    };

    HelperBase* m_pHelperBase;

public:
    template<typename Class>
    Delegate(Class* pObject, ReturnType(Class::*pMethod)())
    {
        m_pHelperBase = new Helper<Class>(pObject, pMethod);
    }

    Delegate(const Delegate& d)
    {
        m_pHelperBase = d.m_pHelperBase->Clone();
    }

    Delegate(Delegate&& d)
    {
        m_pHelperBase = d.m_pHelperBase;
        d.m_pHelperBase = nullptr;
    }

    ~Delegate()
    {
        delete m_pHelperBase;
    }

    Delegate& operator=(const Delegate& d)
    {
        if (this != &d)
        {
            delete m_pHelperBase;
            m_pHelperBase = d.m_pHelperBase->Clone();
        }

        return *this;
    }

    Delegate& operator=(Delegate&& d)
    {
        if (this != &d)
        {
            delete m_pHelperBase;
            m_pHelperBase = d.m_pHelperBase;
            d.m_pHelperBase = nullptr;
        }

        return *this;
    }

    ReturnType operator()() const
    {
        (*m_pHelperBase)();
    }

    bool operator==(const Delegate& d) const
    {
        return *m_pHelperBase == *d.m_pHelperBase;
    }

    bool operator!=(const Delegate& d) const
    {
        return !(*this == d);
    }
};

.NETデリゲートのように使用できます。

class A
{
public:
    void M() { ... }
};

class B
{
public:
    void M() { ... }
};

A a;
B b;

Delegate<void()> d = Delegate<void()>(&a, &A::M);
d(); // calls A::M

d = Delegate<void()>(&b, &B::M);
d(); // calls B::M

これは、引数のないメソッドで機能します。C ++ 11を使用できる場合は、可変個引数テンプレートを使用して任意の数のパラメーターを処理するように変更できます。C ++ 11がない場合は、特定の数のパラメーターを処理するために、Delegateスペシャライゼーションを追加する必要があります。

template<typename ReturnType, typename Arg1>
class Delegate<ReturnType(Arg1)>
{
    ...
};

template<typename ReturnType, typename Arg1, typename Arg2>
class Delegate<ReturnType(Arg1, Arg2)>
{
    ...
};

このデリゲートクラスを使用すると、デリゲートに基づく.NETイベントをエミュレートすることもできます。

于 2013-01-12T02:44:40.830 に答える