web-dev-qa-db-ja.com

C++でインターフェースをどのように宣言しますか?

インターフェイスを表すクラスを設定する方法これは単なる抽象基底クラスですか?

769
Aaron Fischer

bradtgmurray で答えを拡張するには、仮想デストラクタを追加して、インタフェースの純粋仮想メソッドリストに1つの例外を加えることをお勧めします。これにより、具体的な派生クラスを公開せずにポインタの所有権を別のパーティに渡すことができます。インターフェースには具体的なメンバーがないため、デストラクタは何もする必要はありません。関数を仮想とインラインの両方として定義することは矛盾するように思われるかもしれませんが、私を信頼してください - そうではありません。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

仮想デストラクタの本体を含める必要はありません。空のデストラクタを最適化するのに問題があるコンパイラもあります。デフォルトを使用することをお勧めします。

659
Mark Ransom

純粋な仮想メソッドでクラスを作ります。これらの仮想メソッドをオーバーライドする別のクラスを作成してインターフェースを使用してください。

純粋仮想メソッドは、仮想として定義され、0に割り当てられるクラスメソッドです。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};
231
bradtgmurray

C#/ Java の抽象基底クラスに加えて特別なInterface type-categoryがあるのは、C#/ Javaが多重継承をサポートしていないからです。

C++は多重継承をサポートしているので、特別な型は必要ありません。非抽象(純粋仮想)メソッドを持たない抽象基本クラスは、C#/ Javaインタフェースと機能的に同等です。

143
Joel Coehoorn

C++にはそれ自体「インターフェース」の概念はありません。私の知るところでは、多重継承の欠如を回避するために、インターフェースが最初にJavaで導入されました。この概念は非常に有用であることが判明しており、抽象基底クラスを使用することによって同じ効果をC++で達成することができます。

抽象基本クラスは、少なくとも1つのメンバ関数(Javaの用語ではmethod)が次の構文を使用して宣言された純粋仮想関数であるクラスです。

class A
{
  virtual void foo() = 0;
};

抽象基底クラスはインスタンス化できません。 e。クラスAのオブジェクトを宣言することはできません。クラスAを派生させることしかできませんが、foo()の実装を提供しない派生クラスも抽象的になります。抽象クラスであることをやめるために、派生クラスはそれが継承するすべての純粋仮想関数の実装を提供しなければなりません。

抽象基本クラスは、純粋な仮想ではないデータメンバおよびメンバ関数を含むことができるため、インタフェース以上のものになる可能性があります。インターフェースと同等のものは、純粋な仮想関数のみを持つデータを含まない抽象基本クラスです。

そして、Mark Ransomが指摘したように、抽象基底クラスは他の基底クラスと同じように仮想デストラクタを提供するべきです。

46
Dima

テストできる範囲では、仮想デストラクタを追加することが非常に重要です。私はnewで作成されdeleteで破壊されたオブジェクトを使用しています。

インタフェースに仮想デストラクタを追加しないと、継承クラスのデストラクタは呼び出されません。

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

virtual ~IBase() {};なしで前のコードを実行すると、デストラクタTester::~Tester()が呼び出されないことがわかります。

42
Carlos C Soto

私の答えは基本的に他のものと同じですが、私には他に2つの重要なことがあると思います。

  1. あなたのインターフェースで仮想デストラクタを宣言するか、誰かがIDemo型のオブジェクトを削除しようとした場合に未定義の振る舞いを避けるために保護された非仮想のものを作ります。

  2. 多重継承の問題を回避するために仮想継承を使用してください。 (インターフェイスを使用すると、多重継承が頻繁に発生します。)

そして他の答えのように:

  • 純粋な仮想メソッドでクラスを作ります。
  • これらの仮想メソッドをオーバーライドする別のクラスを作成してインターフェースを使用してください。

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }
    

    または

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }
    

    そして

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }
    
32
Rexxar

C++ 11では、継承を完全に避けることができます。

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

この場合、インタフェースは参照セマンティクスを持っています。つまり、オブジェクトがインタフェースより長持ちすることを確認する必要があります(値セマンティクスを持つインタフェースを作成することも可能です)。

これらのタイプのインターフェースには、それぞれ長所と短所があります。

最後に、継承は複雑なソフトウェア設計におけるあらゆる悪の根源です。 Sean Parent's Valueのセマンティクスおよび概念ベースの多態性 (このテクニックのより良いバージョンが説明されていることを強くお勧めします)では、次のケースが検討されます。

MyShapeインターフェースを使って多形的に自分の図形を扱うアプリケーションがあるとします。

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

アプリケーションでは、YourShapeインターフェースを使用して、さまざまな形状で同じことを行います。

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

今、あなたは私があなたのアプリケーションで開発した形のいくつかを使いたいとします。概念的には、私たちの図形は同じインターフェースを持っていますが、私の図形をあなたのアプリケーションで動作させるには、次のように私の図形を拡張する必要があります。

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

第一に、私の形を修正することは全く不可能かもしれません。さらに、多重継承はスパゲッティコードへの道を導きます(3つ目のプロジェクトがTheirShapeインターフェースを使っていることを想像してみてください。彼らがdraw関数をmy_drawと呼んだ場合どうなるでしょう?)。

更新:非継承ベースのポリモーフィズムについての新しい参照がいくつかあります。

10
gnzlbg

上記のすべての良い答え。あなたが心に留めておくべきもう一つのこと - あなたは純粋な仮想デストラクタを持つこともできます。唯一の違いは、それを実装する必要があるということです。

混乱した?


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

あなたがこれをしたいと思う主な理由はあなたが私が持っているようにインターフェースメソッドを提供したいが、それらをオーバーライドすることをオプションにしたいならばです。

クラスをインタフェースクラスにするには純粋仮想メソッドが必要ですが、すべての仮想メソッドにはデフォルトの実装があるため、純粋仮想にするために残されている唯一のメソッドはデストラクタです。

派生クラスでデストラクタを再実装することは大したことではありません - 私は、派生クラスで仮想かどうかに関係なく、デストラクタを常に再実装します。

9
Rodyland

MicrosoftのC++コンパイラを使用している場合は、次のことができます。

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

私はこのアプローチが好きです。インターフェースコードがずっと小さくなり、生成されるコードサイズがかなり小さくなる可能性があるからです。 novtableを使用すると、そのクラスのvtableポインタへの参照がすべて削除されるため、直接インスタンス化することはできません。こちらのドキュメントを参照してください - novtable

7
Mark Ingram

そこに書かれていることに少し追加:

まず、デストラクタも純粋仮想であることを確認してください。

2つ目は、実装するときに、(通常ではなく)仮想的に継承することです。

4
Uri

NVI(Non Virtual Interface Pattern)を使用して実装された契約クラスも検討できます。例えば:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1();
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    virtual void do_f(Parameters p); // From contract 1.
    virtual void do_g(Parameters p); // From contract 2.
};
4
Luc Hermitte

私はまだC++開発が初めてです。私はVisual Studio(VS)から始めました。

しかし、誰もVS (.NET) __interfaceを言及していないようです。私は ではない これがインターフェースを宣言するための良い方法であるかどうか非常に確信しています。しかし、それは追加の執行ドキュメント で言及されている)を提供するようです。自動的に変換されるので、明示的にvirtual TYPE Method() = 0;を指定する必要はありません。

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

しかし、私はクロスプラットフォームコンパイルの互換性について心配しているので、私はそれを使いません。

誰かがそれについて何か面白いことを持っていたら、共有してください。 :-)

ありがとう。

1
Yeo

virtualがインターフェイスを定義するための事実上の標準であることは事実ですが、C++のコンストラクターに付属する古典的なCのようなパターンを忘れないでください。

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

これには、クラスを再構築せずにイベントランタイムを再バインドできるという利点があります(C++にはポリモーフィックタイプを変更するための構文がないため、これはカメレオンクラスの回避策です)。

ヒント:

  • これを基本クラスとして継承し(仮想および非仮想の両方が許可されます)、子孫のコンストラクターにclickを入力します。
  • 関数ポインターをprotectedメンバーとして使用し、public参照および/またはゲッターを持つことができます。
  • 前述のように、これにより、実行時に実装を切り替えることができます。したがって、状態を管理する方法でもあります。 ifsの数とコードの状態の変化に応じて、これはmightswitch()esまたはifsよりも高速です(約3〜4 ifs、ただし常に最初に測定します。
  • 関数ポインターよりもstd::function<>を選択した場合、mightIBase内のすべてのオブジェクトデータを管理できます。この時点から、IBaseの値の回路図を作成できます(たとえば、std::vector<IBase>が機能します)。このmightは、コンパイラとSTLコードによっては遅くなることに注意してください。また、std::function<>の現在の実装は、関数ポインターや仮想関数(これは将来変更される可能性があります)と比較した場合、オーバーヘッドを持つ傾向があります。
0
lorro

これがc ++標準のabstract classの定義です。

n4687

13.4.2

抽象クラスは、他のクラスの基底クラスとしてのみ使用できるクラスです。抽象クラスのオブジェクトは、それから派生したクラスのサブオブジェクトとして以外は作成できません。クラスに少なくとも1つの純粋仮想関数がある場合、そのクラスは抽象クラスです。

0
陳 力