C ++の安全なインターフェイスのパターンは何ですか


22

注:以下はC ++ 03コードですが、今後2年以内にC ++ 11への移行を予定しているため、この点に留意する必要があります。

私は、C ++で抽象的なインターフェースを作成する方法についてのガイドライン(特に初心者向け)を書いています。この件についてサッターの両方の記事を読み、インターネットで例と回答を検索し、いくつかのテストを行いました。

このコードはコンパイルしてはいけません!

void foo(SomeInterface & a, SomeInterface & b)
{
   SomeInterface c ;               // must not be default-constructible
   SomeInterface d(a);             // must not be copy-constructible
   a = b ;                         // must not be assignable
}

上記のすべての動作は、スライスの問題の原因を見つけます:抽象クラス(または階層内の非リーフクラス)は、派生クラスが可能な場合でも、構築もコピー/割り当てもできません。

0番目の解決策:基本的なインターフェース

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

このソリューションは単純で、やや素朴です:すべての制約に違反しています:デフォルトで構築、コピーで構築、およびコピーで割り当て可能それ)。

  1. デストラクタをインラインに維持する必要があるため、デストラクタを宣言できません。また、一部のコンパイラは、インラインの空のボディで純粋な仮想メソッドをダイジェストしません。
  2. はい、このクラスの唯一のポイントは、実装者を事実上破壊可能にすることです。これはまれなケースです。
  3. 追加の仮想純粋メソッド(大部分のケース)があったとしても、このクラスはコピー割り当て可能です。

だから、いや...

最初の解決策:boost :: noncopyable

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

このソリューションは、プレーン、クリア、C ++(マクロなし)であるため、最適です。

問題は、VirtuallyConstructibleがまだデフォルトで構築されている可能性があるため、その特定のインターフェイスではまだ機能しないことです

  1. デストラクタをインラインに維持する必要があるため、デストラクタを純粋な仮想として宣言することはできません。また、一部のコンパイラはそれをダイジェストしません。
  2. はい、このクラスの唯一のポイントは、実装者を事実上破壊可能にすることです。これはまれなケースです。

別の問題は、コピーできないインターフェイスを実装するクラスが、それらのメソッドを必要とする場合、コピーコンストラクターと代入演算子を明示的に宣言/定義する必要があることです(そして、コードには、クライアントからアクセスできる値クラスがあります)インターフェイス)。

これは、ゼロのルールに反します。これは、私たちが行きたいところです。デフォルトの実装で問題がなければ、それを使用できるはずです。

2番目の解決策:それらを保護します!

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

   protected :
      // With C++11, these methods would be "= default"
      MyInterface() {}
      MyInterface(const MyInterface & ) {}
      MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;

このパターンは、(少なくともユーザーコードでは)技術的な制約に従います。MyInterfaceをデフォルトで構築したり、コピーで構築したり、コピーで割り当てたりすることはできません。

また、クラスの実装に人為的な制約を課しません。これらのクラスは、ゼロの規則に従うことも、C ++ 11/14で問題なく「=デフォルト」としていくつかのコンストラクター/演算子を宣言することもできます。

現在、これは非常に冗長であり、代替手段は次のようなマクロを使用することです。

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

   protected :
      DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;

保護対象はマクロの外側にある必要があります(スコープがないため)。

正しく「名前空間」(つまり、会社または製品の名前がプレフィックスとして付けられます)、マクロは無害です。

そして、利点は、すべてのインターフェイスにコピーペーストされるのではなく、コードが1つのソースに組み込まれることです。move-constructorとmove-assignmentが将来同じ方法で明示的に無効にされた場合、これはコードの非常に軽い変更になります。

結論

  • インターフェイスでのスライシングからコードを保護したいのは妄想ですか?(私はそうではないと思うが、誰も知らない...)
  • 上記の中で最良の解決策は何ですか?
  • 別のより良い解決策はありますか?

これは(特に)初心者向けのガイドラインとなるパターンであるため、「各ケースに実装が必要」などのソリューションは実行可能なソリューションではないことに注意してください。

報奨金と結果

質問に回答するのに費やした時間と回答の関連性から、私はコアダンプへの賞金を授与しました。

問題に対する私の解決策は、おそらくそのようなものに行きます:

class MyInterface
{
   DECLARE_CLASS_AS_INTERFACE(MyInterface) ;

   public :
      // the virtual methods
} ;

...次のマクロを使用します。

#define DECLARE_CLASS_AS_INTERFACE(ClassName)                                \
   public :                                                                  \
      virtual ~ClassName() {}                                                \
   protected :                                                               \
      ClassName() {}                                                         \
      ClassName(const ClassName & ) {}                                       \
      ClassName & operator = (const ClassName & ) { return *this ; }         \
   private :

これは、次の理由で私の問題の実行可能な解決策です。

  • このクラスはインスタンス化できません(コンストラクターは保護されています)
  • このクラスは事実上破棄できます
  • このクラスは、継承クラスに過度の制約を課すことなく継承できます(たとえば、継承クラスはデフォルトでコピー可能です)
  • マクロを使用すると、インターフェイスの「宣言」が簡単に認識(および検索)され、そのコードが1か所に組み込まれ、変更が容易になります(適切な接頭辞の付いた名前は、望ましくない名前の衝突を削除します)

他の回答が貴重な洞察を与えたことに注意してください。試してくれた皆さん、ありがとう。

私はまだこの質問に別の報奨金を置くことができると思うことに注意してください、そして私はそれを見る必要があるので、その答えに割り当てるためだけに報奨金を開きます。


5
インターフェイスで単純な仮想関数を使用することはできませんか?virtual void bar() = 0;例えば?これにより、インターフェイスがインスタンス化されなくなります。
モーウェン14

@Morwenn:質問で述べたように、それはケースの99%を解決します(可能であれば100%を目指します)。欠落している1%を無視することを選択した場合でも、割り当てのスライスは解決されません。だから、いや、これは良い解決策ではありません。
paercebal

@Morwenn:真剣に?... :-D ...私は最初にStackOverflowでこの質問を書き、それを提出する直前に考えを変えました。ここで削除して、SOに送信する必要があると思いますか?
paercebal 14

必要なのはvirtual ~VirtuallyDestructible() = 0、インターフェイスクラスの仮想継承(抽象メンバーのみ)だけです。おそらく、VirtuallyDestructibleは省略できます。
ディーターリュッキング14

5
@paercebal:コンパイラーが純粋な仮想クラスを停止した場合、ごみ箱に属します。実際のインターフェイスは、定義により純粋仮想です。
誰も14

回答:


13

C ++でインターフェイスを作成する標準的な方法は、純粋な仮想デストラクタを提供することです。これにより、

  • C ++では抽象クラスのインスタンスを作成できないため、インターフェイスクラス自体のインスタンスは作成できません。これにより、構築不可能な要件(デフォルトとコピーの両方)が処理されます。
  • deleteインターフェイスへのポインタを呼び出すと、正しいことが行われます。そのインスタンスの最も派生したクラスのデストラクタが呼び出されます。

純粋な仮想デストラクタがあるだけで、インターフェイスへの参照の割り当てを妨げることはありません。同様に失敗する必要がある場合は、保護された代入演算子をインターフェイスに追加する必要があります。

C ++コンパイラはすべて、次のようなクラス/インターフェイスを処理できる必要があります(すべて1つのヘッダーファイルに含まれます)。

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

これを止めるコンパイラ(つまり、C ++ 98より前のバージョンでなければならない)がある場合、オプション2(コンストラクターが保護されている)は次善策です。

使用boost::noncopyableは、このタスクにはお勧めできません。階層内のすべてのクラスをコピー不可にする必要があるというメッセージが送信されるため、このように使用する意図に詳しくない経験豊富な開発者に混乱を引き起こす可能性があるためです。


If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.:これが私の問題の根本です。割り当てをサポートするためにインターフェイスが必要な場合は、実際にはまれです。一方、インターフェイスを参照渡ししたい場合(NULLが受け入れられない場合)、したがって、コンパイルを行わないノーオペレーションまたはスライスを避けたい場合は、はるかに大きくなります。
paercebal 14

代入演算子は決して呼び出されるべきではないのに、なぜそれを定義するのですか?余談ですが、作ってみませんprivateか?さらに、デフォルトおよびコピーアクターを扱うこともできます。
デュプリケータ

5

私は妄想しています...

  • インターフェイスでのスライシングからコードを保護したいのは妄想ですか?(私はそうではないと思うが、誰も知らない...)

これはリスク管理の問題ではありませんか?

  • スライシングに関連するバグが導入される可能性があることを恐れていますか?
  • 気付かれずに回復不能なバグを引き起こす可能性があると思いますか?
  • スライスを避けるためにどの程度進んでいきますか?

最適なソリューション

  • 上記の中で最良の解決策は何ですか?

2番目のソリューション(「それらを保護する」)は良さそうに見えますが、私はC ++の専門家ではないことに留意してください。
少なくとも、無効な使用法は、コンパイラー(g ++)によってエラーとして正しく報告されているようです。

さて、マクロが必要ですか?あなたが書いているガイドラインの目的は何と言っていなくても、製品のコードに特定のベストプラクティスのセットを強制するためだと思うので、「はい」と言います。

そのために、マクロは、人々がパターンを効果的に適用する時期を検出するのに役立ちます。コミットの基本的なフィルターは、マクロが使用されたかどうかを示します。

  • 使用すると、パターンが適用される可能性が高くなり、さらに重要なことに、正しく適用されます(protectedキーワードがあることを確認するだけです)。
  • 使用されていない場合は、使用されなかった理由を調査できます。

マクロがなければ、すべての場合にパターンが必要であり、適切に実装されているかどうかを調べる必要があります。

より良いソリューション

  • 別のより良い解決策はありますか?

C ++でのスライスは、言語の特性にすぎません。ガイドライン(特に初心者向け)を書いているので、「コーディングルール」を列挙するだけでなく、教育に集中する必要があります。スライスがどのように、なぜ起こるのかを、例と演習とともに実際に説明する必要があります(車輪を再発明しないで、本やチュートリアルからインスピレーションを得てください)。

たとえば、エクササイズのタイトルは、「C ++の安全なインターフェイスのパターンは何ですか?」などです。

そのため、C ++開発者がスライスが発生したときに何が起こっているのかを確実に理解できるようにするのが最善の方法です。もしそうすれば、その特定のパターンを正式に強制しなくても、あなたが恐れるほど多くの間違いをコードで犯さないと確信しています(しかし、それを強制することはできますが、コンパイラの警告は良いです)。

コンパイラについて

あなたは言う :

私はこの製品のコンパイラーを選択する権限がありません。

多くの場合、人々は「私にはする権利がない[X]」「私はするべきではない[Y] ...」、...彼らこれが不可能だと思うので、試したまたは尋ねた。

技術的な問題に関する意見を述べることは、おそらく仕事の説明の一部です。コンパイラが問題の領域に最適な(または独自の)選択肢であると本当に思う場合は、それを使用してください。しかし、「インライン実装を備えた純粋な仮想デストラクタは、私が見た中で最悪の窒息ポイントではない」とも言っています。私の理解では、コンパイラは非常に特殊であるため、知識のあるC ++開発者でさえ使用するのが困難です。レガシー/社内コンパイラは現在技術的な負債であり、他の開発者やマネージャとその問題について議論する権利がありますか? 。

コンパイラを維持するコストと別のコンパイラを使用するコストを評価してみてください。

  1. 現在のコンパイラは、他の誰にもできないことを何をもたらしますか?
  2. 製品コードは別のコンパイラを使用して簡単にコンパイルできますか?どうして

私はあなたの状況を知りません。実際、おそらくあなたは特定のコンパイラに結び付けられる正当な理由を持っているでしょう。
しかし、これが単なる慣性である場合、あなたや同僚が生産性や技術的な負債の問題を報告しなければ、状況は決して変化しません。


Am I paranoid...:「インターフェイスを正しく使いやすく、誤って使いにくいものにします」。私の静的メソッドの1つが誤って誤って使用されたと誰かが報告したとき、私はその特定の原則を味わいました。発生したエラーは無関係と思われ、エンジニアがソースを見つけるのに数時間かかりました。この「インターフェースエラー」は、インターフェースリファレンスを別のインターフェースリファレンスに割り当てることに匹敵します。それで、はい、私はその種のエラーを避けたいです。また、C ++では、哲学はコンパイル時に可能な限りキャッチすることであり、言語はその力を与えてくれるので、私たちはそれを採用します。
paercebal

Best solution: 同意する。。。Better solution:それは素晴らしい答えです。私はそれに取り組みます...今、についてPure virtual classes:これは何ですか?C ++抽象インターフェース?(状態のないクラスと純粋な仮想メソッドのみ?)。この「純粋な仮想クラス」は、スライスから私をどのように保護しましたか?(純粋な仮想メソッドは、インスタンス化をコンパイルせず、コピー割り当てと移動割り当ても行いますIIRC)。
paercebal

About the compiler:私たちは同意しますが、コンパイラは私の責任の範囲外です(それは私がこっけいなコメントを止めないということではありません... :-p ...)。詳細は公開しませんが(可能であれば)、内部の理由(テストスイートなど)と外部の理由(クライアントとライブラリとのリンクなど)に関係しています。結局、コンパイラのバージョンを変更する(またはパッチを適用する)ことは簡単な操作ではありません。壊れたコンパイラを最近のgccに置き換えるのはもちろんです。
paercebal

@paercebalコメントありがとうございます。純粋な仮想クラスについては、あなたは正しいです、それはあなたのすべての制約を解決するわけではありません(私はこの部分を削除します)。私は「インターフェースエラー」の部分とコンパイル時のエラーのキャッチがどのように役立つかを理解しています。コンパイラのことで頑張ってください:)
coredump

1
私はマクロのファンではありません。特に、ガイドラインは(また)ジュニアを対象としているためです。あまりにも頻繁に、盲目的にそれらを適用するためにそのような「便利な」ツールを与えられ、実際に何が起こっているのかを決して理解しない人々を見てきました。彼らは、上司が自分でやるのは難しすぎると思っていたので、マクロがすることは最も複雑なことでなければならないと信じるようになりました。また、マクロは会社にのみ存在するため、Web検索を行うことさえできませんが、文書化されたガイドラインでは、宣言するメンバー関数とその理由は可能です。
5gon12eder

2

スライスの問題は1つですが、ランタイムポリモーフィックインターフェイスをユーザーに公開するときに生じる問題は確かに1つだけではありません。nullポインター、メモリ管理、共有データを考えてください。これらはいずれもすべての場合に簡単に解決できるものではありません(スマートポインターは優れていますが、それでも特効薬ではありません)。実際、あなたの投稿からは、スライシングの問題を解決しようとしているようには見えませんが、ユーザーがコピーを作成できないようにすることでそれを回避しています。スライスの問題を解決するために必要なことは、仮想クローンメンバー関数を追加することだけです。ランタイムポリモーフィックインターフェイスを公開することのより深い問題は、値のセマンティクスよりも推論するのが難しい参照セマンティクスをユーザーに処理させることです。

C ++でこれらの問題を回避するために知っている最良の方法は、型消去を使用することです。これは、通常のクラスインターフェイスの背後で、実行時のポリモーフィックインターフェイスを非表示にする手法です。この通常のクラスインターフェイスは値セマンティクスを持ち、画面の背後にあるすべての多態的な「混乱」を処理します。std::function型消去の典型的な例です。

継承をユーザーに公開することが悪い理由と、Sean Parentによるこれらのプレゼンテーションを参照して、タイプ消去がどのように修正するのに役立つかについての優れた説明:

継承は悪の基本クラス(ショートバージョン)

価値のセマンティクスと概念ベースのポリモーフィズム(長いバージョン。わかりやすくなっていますが、音は良くありません)


0

あなたは妄想ではありません。C ++プログラマーとしての最初の専門的な仕事の結果、スライスとクラッシュが発生しました。私は他の人を知っています。このための良い解決策はあまりありません。

コンパイラの制約を考えると、オプション2が最適です。新しいプログラマーが奇妙で神秘的に見えるマクロを作成する代わりに、コードを自動生成するためのスクリプトまたはツールを提案します。新しい従業員がIDEを使用する場合、インターフェイス名を要求する「新しいMYCOMPANYインターフェイス」ツールを作成し、探している構造を作成できるはずです。

プログラマがコマンドラインを使用している場合は、使用可能なスクリプト言語を使用して、NewMyCompanyInterfaceスクリプトを作成し、コードを生成します。

過去にこのアプローチを一般的なコードパターン(インターフェイス、ステートマシンなど)に使用しました。良い点は、新しいプログラマーが出力を読んで簡単に理解できることです。生成できないものが必要なときに必要なコードを再現できます。

マクロやその他のメタプログラミングのアプローチは、何が起こっているのかをわかりにくくする傾向があり、新しいプログラマーは「カーテンの後ろ」で何が起こっているのかを学習しません。彼らがパターンを破らなければならないとき、彼らは以前と同じように失われます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.