36

オーバーロードされた無料の関数 (アドホック ポリモーフィズム) を介して同じ操作をすべてサポートする、関連のない型がいくつかあります。

struct A {};

void use(int x) { std::cout << "int = " << x << std::endl; }
void use(const std::string& x) { std::cout << "string = " << x << std::endl; }
void use(const A&) { std::cout << "class A" << std::endl; }

use()質問のタイトルが示すように、これらの型のインスタンスを異種コンテナに格納して、具体的な型に関係なくそれらを格納したいと考えています。コンテナーには値のセマンティクスが必要です (つまり、2 つのコンテナー間の割り当てはデータをコピーし、共有しません)。

std::vector<???> items;
items.emplace_back(3);
items.emplace_back(std::string{ "hello" });
items.emplace_back(A{});

for (const auto& item: items)
    use(item);
// or better yet
use(items);

もちろん、これは完全に拡張可能でなければなりません。を受け取るライブラリ API と、vector<???>既知の型に独自の型を追加するクライアント コードを考えてみてください。


通常の解決策は、(スマート) ポインターを (抽象) インターフェース (例: ) に格納することですvector<unique_ptr<IUsable>>が、これには多くの欠点があります-私の頭の上から:

  • 現在のアドホック ポリモーフィック モデルを、すべてのクラスが共通インターフェイスから継承するクラス階層に移行する必要があります。ああスナップ!intここで、andのラッパーを作成する必要がstringあります。自由なメンバー関数がインターフェイス (仮想メンバー関数) と密接に結び付いているため、再利用性/構成可能性が低下することは言うまでもありません。
  • コンテナーはその値のセマンティクスを失います:vec1 = vec2使用すると単純な代入が不可能になりunique_ptr(ディープ コピーを手動で実行する必要があります)、使用すると両方のコンテナーが共有状態になりますshared_ptr(これには長所と短所がありますが、値が必要なのでコンテナのセマンティクス、ここでもディープ コピーを手動で実行する必要があります)。
  • ディープ コピーを実行できるようにするには、すべての派生クラスに実装する必要がある仮想clone()関数をインターフェイスがサポートする必要があります。それよりももっとつまらないことを真剣に考えられますか?

要約すると、これにより多くの不必要な結合が追加され、大量の (おそらく役に立たない) ボイラープレート コードが必要になります。これは間違いなく満足のいくものではありませんが、これまでのところ、これが私が知っている唯一の実用的な解決策です。


私は長い間、サブタイプのポリモーフィズム (別名、インターフェース継承) に代わる実行可能な方法を探してきました。私はアドホック ポリモーフィズム (別名、オーバーロードされた無料関数) でよく遊んでいますが、常に同じハードウォールにぶつかっています: コンテナーは同種でなければならないため、私は常にしぶしぶ継承とスマート ポインターに戻りますが、既に上に挙げたすべての欠点があります (そしておそらくそれ以上)。

理想的には、現在の (存在しない) 型階層に何も変更せずvector<IUsable>に、適切な値のセマンティクスを使用して、サブタイプのポリモーフィズムを要求する代わりにアドホックなポリモーフィズムを維持したいと考えています。

これは可能ですか?もしそうなら、どのように?

4

5 に答える 5

18

正当な理由: Sean Parent のGoing Native 2013の「継承は悪の基本クラス」の講演を見たとき、後から考えると、この問題を解決するのが実際にいかに簡単であるかを実感しました。2013 年のゴーイング ネイティブの他のトークと同様に、この Q/A はトーク全体のほんの一部に過ぎませんが、それを視聴することをお勧めします。


実際には、説明がほとんど必要ないほど単純で、コード自体が物語っています。

struct IUsable {
  template<typename T>
  IUsable(T value) : m_intf{ new Impl<T>(std::move(value)) } {}
  IUsable(IUsable&&) noexcept = default;
  IUsable(const IUsable& other) : m_intf{ other.m_intf->clone() } {}
  IUsable& operator =(IUsable&&) noexcept = default;
  IUsable& operator =(const IUsable& other) { m_intf = other.m_intf->clone(); return *this; }

  // actual interface
  friend void use(const IUsable&);

private:
  struct Intf {
    virtual ~Intf() = default;
    virtual std::unique_ptr<Intf> clone() const = 0;
    // actual interface
    virtual void intf_use() const = 0;
  };
  template<typename T>
  struct Impl : Intf {
    Impl(T&& value) : m_value(std::move(value)) {}
    virtual std::unique_ptr<Intf> clone() const override { return std::unique_ptr<Intf>{ new Impl<T>(*this) }; }
    // actual interface
    void intf_use() const override { use(m_value); }
  private:
    T m_value;
  };
  std::unique_ptr<Intf> m_intf;
};

// ad hoc polymorphic interface
void use(const IUsable& intf) { intf.m_intf->intf_use(); }

// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsable, Args...>& c) {
  std::cout << "vector<IUsable>" << std::endl;
  for (const auto& i: c) use(i);
  std::cout << "End of vector" << std::endl;
}

int main() {
  std::vector<IUsable> items;
  items.emplace_back(3);
  items.emplace_back(std::string{ "world" });
  items.emplace_back(items); // copy "items" in its current state
  items[0] = std::string{ "hello" };
  items[1] = 42;
  items.emplace_back(A{});
  use(items);
}

// vector<IUsable>
// string = hello
// int = 42
// vector<IUsable>
// int = 3
// string = world
// End of vector
// class A
// End of vector

ご覧のとおり、これは a のかなり単純なラッパーでありunique_ptr<Interface>、派生した をインスタンス化するテンプレート化されたコンストラクターを備えていますImplementation<T>。すべての (完全ではない) 厄介な詳細は非公開であり、パブリック インターフェイスはこれ以上きれいにはなりません: ラッパー自体には、構築/コピー/移動以外のメンバー関数はありません。インターフェイスはuse()、既存のものをオーバーロードするフリー関数として提供されます。

明らかに、 の選択は、オブジェクトのコピーを作成したいときはいつでも呼び出されるunique_ptrプライベート関数を実装する必要があることを意味します (これにはヒープ割り当てが必要です)。確かに、コピーごとに 1 つのヒープ割り当ては非常に最適ではありませんが、パブリック インターフェイスのいずれかの関数が基になるオブジェクトを変更できる場合 (つまり、非 const参照を取得してそれらを変更した場合)、これは要件です。このようにして、すべてのオブジェクトが一意であることを保証します。したがって、自由に変異することができます。clone()IUsableuse()


問題のように、オブジェクトが完全に不変である場合 (公開されたインターフェイスだけでなく、オブジェクト全体が常に完全に不変であることを意味します)、悪意のある副作用なしに共有状態を導入できます。これを行う最も簡単な方法は、の代わりに- to-constを使用することです。shared_ptrunique_ptr

struct IUsableImmutable {
  template<typename T>
  IUsableImmutable(T value) : m_intf(std::make_shared<const Impl<T>>(std::move(value))) {}
  IUsableImmutable(IUsableImmutable&&) noexcept = default;
  IUsableImmutable(const IUsableImmutable&) noexcept = default;
  IUsableImmutable& operator =(IUsableImmutable&&) noexcept = default;
  IUsableImmutable& operator =(const IUsableImmutable&) noexcept = default;

  // actual interface
  friend void use(const IUsableImmutable&);

private:
  struct Intf {
    virtual ~Intf() = default;
    // actual interface
    virtual void intf_use() const = 0;
  };
  template<typename T>
  struct Impl : Intf {
    Impl(T&& value) : m_value(std::move(value)) {}
    // actual interface
    void intf_use() const override { use(m_value); }
  private:
    const T m_value;
  };
  std::shared_ptr<const Intf> m_intf;
};

// ad hoc polymorphic interface
void use(const IUsableImmutable& intf) { intf.m_intf->intf_use(); }

// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsableImmutable, Args...>& c) {
  std::cout << "vector<IUsableImmutable>" << std::endl;
  for (const auto& i: c) use(i);
  std::cout << "End of vector" << std::endl;
}

関数がどのように消えたかに注意してclone()ください (もう必要ありません。基礎となるオブジェクトを共有するだけで、不変なので問題ありません) 。保証noexceptのおかげでコピーがどのようになったかに注目してください。shared_ptr

おもしろいのは、基礎となるオブジェクトは不変でなければならないということですが、それでもIUsableImmutableラッパーを変更できるので、これを行ってもまったく問題ありません。

  std::vector<IUsableImmutable> items;
  items.emplace_back(3);
  items[0] = std::string{ "hello" };

(shared_ptr基になるオブジェクト自体ではなく、 のみが変更されるため、他の共有参照には影響しません)

于 2013-09-17T18:05:53.007 に答える
3

以前の他の回答 (vtabled インターフェイス基本クラスの使用、boost::variant の使用、仮想基本クラスの継承トリックの使用) はすべて、この問題に対する完全に優れた有効なソリューションであり、コンパイル時間と実行時間のコストのバランスが異なります。ただし、C++ 11 以降では、boost::variant の代わりに、C++ 11/14 を使用した boost::variant の再実装である Egg ::variant を代わりに使用することをお勧めします。これは、設計、パフォーマンス、使いやすさの点で非常に優れています。 、抽象化の力であり、VS2013 ではかなり完全な機能サブセット (および VS2015 では完全な機能セット) を提供します。また、主要な Boost の作成者によって作成および保守されています。

ただし、問題を少し再定義できる場合-具体的には、より強力なものを優先して型消去 std::vector を失う可能性がある場合-代わりに異種の型コンテナーを使用できます。これらはコンテナの変更ごとに新しいコンテナ タイプを返すことで機能するため、パターンは次のようにする必要があります。

newtype newcontainer=oldcontainer.push_back(newitem);

これらを C++ 03 で使用するのは面倒でしたが、Boost.Fusion はそれらを潜在的に有用なものにするためのかなりの努力をしています。実際に有用なユーザビリティは C++ 11 以降でのみ可能です。特に C++ 14 以降では、constexpr 関数型プログラミングを使用してこれらの異種コレクションを非常に簡単にプログラミングできる汎用ラムダのおかげで可能です。理想的にはclang 3.6またはGCC 5.0を必要とするBoost.Hanaを提案しました。

異種タイプのコンテナーは、99% のコンパイル時間と 1% の実行時間コストのソリューションです。多くのコンパイラ オプティマイザが現在のコンパイラ テクノロジに直面しているのを目にするでしょう。たとえば、clang 3.5 が 2 つのオペコードを生成するはずだったコードに対して 2500 個のオペコードを生成し、同じコードに対して GCC 4.9 が 15 個のオペコードを吐き出し、そのうち 12 個はそうではありませんでした。実際には何でもします(メモリをレジスタにロードし、それらのレジスタで何もしませんでした)。とは言っても、数年後には、異種の型のコンテナに最適なコード生成を実現できるようになるでしょう。その時点で、それらは C++ メタプログラミングの次世代の形式になると予想されます。実際の関数を使用して C++ コンパイラを機能的にプログラミングできる!!!

于 2015-02-24T16:10:13.247 に答える
1

std::functionlibstdc++ での実装から最近得たアイデアを次に示します。

T でコピー、削除、およびその他の操作を実行する方法を知っている静的メンバー関数Handler<T>を使用して、テンプレート クラスを作成します。

次に、その静的関数への関数ポインターを Any クラスのコンストラクターに格納します。Any クラスは T について知る必要はありません。T 固有の操作をディスパッチするためにこの関数ポインタが必要なだけです。関数のシグネチャは T に依存しないことに注意してください。

おおよそ次のようになります。

struct Foo { ... }
struct Bar { ... }
struct Baz { ... }

template<class T>
struct Handler
{
    static void action(Ptr data, EActions eAction)
    {
       switch (eAction)
       {
       case COPY:
           call T::T(...);

       case DELETE:
           call T::~T();

       case OTHER:
           call T::whatever();
       }
    }
}

struct Any
{
    Ptr handler;
    Ptr data;

    template<class T>
    Any(T t)
      : handler(Handler<T>::action)
      , data(handler(t, COPY))
    {}

    Any(const Any& that)
       : handler(that.handler)
       , data(handler(that.data, COPY))
    {}

    ~Any()
    {
       handler(data, DELETE);
    }
};

int main()
{
    vector<Any> V;

    Foo foo; Bar bar; Baz baz;

    v.push_back(foo);
    v.push_back(bar);
    v.push_back(baz);
}

これにより、値のセマンティクスを維持しながら型消去が可能になり、含まれるクラス (Foo、Bar、Baz) を変更する必要がなくなり、動的ポリモーフィズムをまったく使用しなくなります。それはかなりクールなものです。

于 2013-09-18T07:01:48.720 に答える