7

この質問は、C++ 言語自体に関するものではなく (つまり、標準に関するものではありません)、コンパイラを呼び出して仮想関数の代替スキームを実装する方法に関するものです。

仮想関数を実装するための一般的なスキームは、ポインターのテーブルへのポインターを使用することです。

class Base {
     private:
        int m;
     public:
        virtual metha();
};

同様に、Cは次のようになります

struct Base {
    void (**vtable)();
    int m;
}

最初のメンバーは通常、仮想関数のリストなどへのポインターです (アプリケーションが制御できないメモリー内の領域)。そしてほとんどの場合、これはメンバーなどを考慮する前にポインターのサイズを消費することになります。したがって、32 ビットのアドレッシング スキームでは、約 4 バイトなどです。アプリケーションで 40k ポリモーフィック オブジェクトのリストを作成した場合、これは約 40k x 4 バイト = メンバー変数などの前に 160k バイト。また、これがたまたま C++ コンパイルの中で最も高速で一般的な実装であることもわかっています。

これは複数の継承によって複雑になることを私は知っています (特に仮想クラス、つまりダイヤモンド構造体など)。

同じことを行う別の方法は、最初の変数を vptrs のテーブルへのインデックス ID として持つことです (以下の C と同等)。

struct Base {
    char    classid;     // the classid here is an index into an array of vtables
    int     m;
}

アプリケーション内のクラスの総数が 255 未満の場合 (可能なすべてのテンプレートのインスタンス化などを含む)、char はインデックスを保持するのに十分であり、アプリケーション内のすべてのポリモーフィック クラスのサイズを縮小します (アライメントの問題は除外しています)。など)。

私の質問は、GNU C++、LLVM、または他のコンパイラにこれを行うためのスイッチはありますか?? またはポリモーフィック オブジェクトのサイズを縮小しますか?

編集:指摘されたアライメントの問題について理解しています。また、これが 64 ビット システム (64 ビット vptr を想定) で、各ポリモーフィック オブジェクト メンバーのコストが約 8 バイトである場合、vptr のコストはメモリの 50% です。これは主に大量に作成された小さなポリモーフィックに関連しているため、アプリケーション全体ではないにしても、少なくとも特定の仮想オブジェクトに対してこのスキームが可能かどうか疑問に思っています。

4

4 に答える 4

3

いいえ、そのようなスイッチはありません。

LLVM/Clang コードベースは、数万単位で割り当てられるクラス内の仮想テーブルを回避します。これは、閉じた階層でうまく機能しenumますenum閉鎖は明らかenumに.

次に、メソッドを呼び出す前に、 で仮想性を実装し、switch適切なキャストを行います。enumもう一度、閉じました。switch新しいクラスごとに を変更する必要があります。


最初の選択肢: 外部 vpointer。

vpointer 税があまりにも頻繁に支払われる状況に陥った場合、それはほとんどのオブジェクトが既知の型です。次に、それを外部化できます。

class Interface {
public:
  virtual ~Interface() {}

  virtual Interface* clone() const = 0; // might be worth it

  virtual void updateCount(int) = 0;

protected:
  Interface(Interface const&) {}
  Interface& operator=(Interface const&) { return *this; }
};

template <typename T>
class InterfaceBridge: public Interface {
public:
  InterfaceBridge(T& t): t(t) {}

  virtual InterfaceBridge* clone() const { return new InterfaceBridge(*this); }

  virtual void updateCount(int i) { t.updateCount(i); }

private:
  T& t; // value or reference ? Choose...
};

template <typename T>
InterfaceBridge<T> interface(T& t) { return InterfaceBridge<T>(t); }

次に、単純なクラスを想像してください。

class Counter {
public:
  int getCount() const { return c; }
  void updateCount(int i) { c = i; }
private:
  int c;
};

オブジェクトを配列に格納できます。

static Counter array[5];

assert(sizeof(array) == sizeof(int)*5); // no v-pointer

そして、多相関数でそれらを使用します。

void five(Interface& i) { i.updateCount(5); }

InterfaceBridge<Counter> ib(array[3]); // create *one* v-pointer
five(ib);

assert(array[3].getCount() == 5);

値と参照は、実際には設計上の緊張です。一般に、必要な場合はclone値で保存する必要があり、基本クラスで保存する場合は複製する必要があります(boost::ptr_vectorたとえば)。実際に両方のインターフェース (およびブリッジ) を提供することは可能です:

Interface <--- ClonableInterface
  |                 |
InterfaceB     ClonableInterfaceB

それは単なる余分な入力です。


はるかに複雑な別のソリューション。

スイッチはジャンプテーブルで実装可能です。このようなテーブルは、実行時に完全に作成できます。std::vectorたとえば、次のようになります。

class Base {
public:
  ~Base() { VTables()[vpointer].dispose(*this); }

  void updateCount(int i) {
    VTables()[vpointer].updateCount(*this, i);
  }

protected:
  struct VTable {
    typedef void (*Dispose)(Base&);
    typedef void (*UpdateCount)(Base&, int);

    Dispose dispose;
    UpdateCount updateCount;
  };

  static void NoDispose(Base&) {}

  static unsigned RegisterTable(VTable t) {
    std::vector<VTable>& v = VTables();
    v.push_back(t);
    return v.size() - 1;
  }

  explicit Base(unsigned id): vpointer(id) {
    assert(id < VTables.size());
  }

private:
  // Implement in .cpp or pay the cost of weak symbols.
  static std::vector<VTable> VTables() { static std::vector<VTable> VT; return VT; }

  unsigned vpointer;
};

そして、Derivedクラス:

class Derived: public Base {
public:
  Derived(): Base(GetID()) {}

private:
  static void UpdateCount(Base& b, int i) {
    static_cast<Derived&>(b).count = i;
  }

  static unsigned GetID() {
    static unsigned ID = RegisterTable(VTable({&NoDispose, &UpdateCount}));
    return ID;
  }

  unsigned count;
};

これで、いくらかのオーバーヘッドを犠牲にしてでも、コンパイラがそれを実行してくれることがいかに素晴らしいかがわかります。

ああ、アラインメントのために、クラスがポインターを導入するとすぐに、次の属性とのDerived間で 4 バイトのパディングが使用されるリスクがあります。パディングを避けるためBaseに最初のいくつかの属性を慎重に選択することで、それらを使用できます...Derived

于 2012-04-12T14:54:38.753 に答える
3

いくつかの観察:

  1. はい、より小さな値を使用してクラスを表すことができますが、一部のプロセッサではデータを整列する必要があるため、データ値を 4 バイト境界などに整列する必要があるため、スペースの節約が失われる可能性があります。さらに、class-id は、ポリモーフィック継承ツリーのすべてのメンバーに対して明確に定義された場所になければならないため、他の日付よりも先になる可能性が高く、アライメントの問題は回避できません。

  2. ポインターを格納するコストはコードに移されました。そこでは、ポリモーフィック関数を使用するたびに、クラス ID を vtable ポインターまたは同等のデータ構造に変換するコードが必要になります。なので無料ではありません。明らかにコストのトレードオフは、コードの量とオブジェクトの数に依存します。

  3. オブジェクトがヒープから割り当てられる場合、通常、オブジェクトが最悪の境界に確実に割り当てられるようにスペースが浪費されます。そのため、少量のコードと多数のポリモーフィック オブジェクトがある場合でも、メモリ管理のオーバーヘッドが大きくなる可能性があります。ポインターと char の差よりもかなり大きい。

  4. プログラムを個別にコンパイルできるようにするには、プログラム全体のクラスの数、つまりクラス ID のサイズをコンパイル時に知っておく必要があります。そうしないと、コードをコンパイルしてアクセスすることができません。これはかなりのオーバーヘッドになります。最悪の場合に備えて修正し、コンパイルとリンクを簡素化する方が簡単です。

試みをやめさせないでください。しかし、可変サイズの ID を使用して関数アドレスを導出する可能性のある手法を使用して解決する必要のある問題は他にもたくさんあります。

Ian Piumarta の ColaWikipedia Colaで見ることを強くお勧めします。

実際には別のアプローチを取り、ポインターをより柔軟な方法で使用して、継承、プロトタイプベース、または開発者が必要とするその他のメカニズムを構築します。

于 2012-04-12T14:20:50.740 に答える
3

あなたの提案は興味深いですが、実行可能ファイルが複数のモジュールで構成されており、それらの間でオブジェクトを渡している場合は機能しません。それらが別々にコンパイルされている場合 (DLL など)、1 つのモジュールがオブジェクトを作成して別のモジュールに渡し、もう 1 つのモジュールが仮想メソッドを呼び出した場合、どのテーブルがclassid参照されているかをどのように知るのでしょうか? moduleid2 つのモジュールはコンパイル時に互いを認識していない可能性があるため、別のモジュールを追加することはできません。なので、ポインタを使わないと行き止まりだと思います...

于 2012-04-12T14:12:52.977 に答える
2

短い答えは、いいえ、一般的な C++ コンパイラでこれを行うためのスイッチを知りません。

より長い答えは、これを行うには、ほとんどのインテリジェンスをリンカーに組み込む必要があるということです。これにより、リンクされるすべてのオブジェクト ファイル間で ID の分散を調整できます。

また、それは一般的にあまり良いことではないことも指摘しておきます. 少なくとも典型的なケースでは、構造体/クラスの各要素を「自然な」境界に配置する必要があります。つまり、開始アドレスはそのサイズの倍数です。単一の int を含むクラスの例を使用すると、コンパイラは vtable インデックスに 1 バイトを割り当て、その後すぐに 3 バイトのパディングが続くため、次intは 4 の倍数のアドレスに到達します。最終結果として、クラスのオブジェクトは、ポインターを使用した場合とまったく同じ量のストレージを占有します。

これもとてつもない例外ではないことを付け加えておきます。何年もの間、構造体/クラスに挿入されるパディングを最小限に抑えるための標準的なアドバイスは、最初に最大であると予想されるアイテムを配置し、最小に向かって進むことでした。つまり、ほとんどのコードでは、構造体の最初の明示的に定義されたメンバーの前に、同じ 3 バイトのパディングが発生することになります。

これから何かを得るには、それを認識し、必要な場所に移動できる (たとえば) 3 バイトのデータを持つ構造体を用意する必要があります。次に、それらを移動して、構造体で明示的に定義された最初の項目にします。残念ながら、これは、このスイッチをオフにして vtable ポインターを使用すると、コンパイラーが不要なパディングを挿入することになることも意味します。

要約すると、それは実装されていません。

于 2012-04-12T14:51:25.807 に答える