25

問題を説明するための適切な質問のタイトルを考えることはできません。うまくいけば、以下の詳細が私の問題を明確に説明しています。

次のコードを検討してください

#include <iostream>

template <typename Derived>
class Base
{
    public :

    void call ()
    {
        static_cast<Derived *>(this)->call_impl();
    }
};

class D1 : public Base<D1>
{
    public :

    void call_impl ()
    {
        data_ = 100;
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

class D2 : public Base<D1> // This is wrong by intension
{
    public :

    void call_impl ()
    {
        std::cout << data_ << std::endl;
    }

    private :

    int data_;
};

int main ()
{
    D2 d2;
    d2.call_impl();
    d2.call();
    d2.call_impl();
}

D2の定義は意図的に間違っていますが、コンパイルして実行します。最初の呼び出しは、初期化されなかっd2.call_impl()たと予想されるいくつかのランダムビットを出力します。D2::data_2番目と3番目の呼び出しはすべて、に対して出力100されますdata_

なぜコンパイルされて実行されるのか理解しています。間違っている場合は修正してください。

呼び出しを行うとd2.call()、呼び出しはに解決されBase<D1>::call、にキャストさthisD1て呼び出されますD1::call_implD1は確かに派生形式であるためBase<D1>、コンパイル時のキャストは問題ありません。

実行時、キャスト後、、thisは、実際にはD2オブジェクトであるかのように扱われD1、への呼び出しは、D1::call_implあるはずのメモリビットを変更してD1::data_出力します。この場合、これらのビットはたまたまどこにありますかD2::data_d2.call_impl()2つ目も、C++の実装によっては未定義の動作になると思います。

重要なのは、このコードは意図的に間違っていますが、ユーザーにエラーの兆候を与えないということです。私のプロジェクトで実際に行っているのは、ディスパッチエンジンのように機能するCRTP基本クラスがあることです。callライブラリ内の別のクラスは、たとえば、CRTP基本クラスのインターフェイスにアクセスし、基本クラスのデフォルト実装または派生クラスの実装callにディスパッチします。call_dispatchユーザー定義の派生クラス、たとえばD、が実際にから派生している場合、これらはすべて正常に機能しBase<D>ます。から派生していないBase<Unrelated>場所から派生している場合は、コンパイル時エラーが発生します。ただし、ユーザーが上記のようなコードを作成することを妨げることはありません。UnrelatedBase<Unrelated>

ユーザーは、基本CRTPクラスから派生し、いくつかの実装の詳細を提供することにより、ライブラリーを使用します。上記のような誤った使用の問題を回避できる他の設計の選択肢が確かにあります(たとえば、抽象基本クラス)。しかし、とりあえずそれらを脇に置いて、何らかの理由でこのデザインが必要だと私に信じさせましょう。

だから私の質問は、上記のようにユーザーが間違った派生クラスを書くのを防ぐことができる方法はありますか?つまり、ユーザーが派生実装クラス、たとえばを作成Dしたが、それをから派生したBase<OtherD>場合、コンパイル時エラーが発生します。

1つの解決策は使用dynamic_castです。ただし、これは広範囲にわたるものであり、機能する場合でも実行時エラーになります。

4

6 に答える 6

35

1)Baseのすべてのコンストラクターをプライベートにします(コンストラクターがない場合は、コンストラクターを追加します)

2)派生テンプレートパラメータをBaseのフレンドとして宣言します

template <class Derived>
class Base
{
private:

  Base(){}; // prevent undesirable inheritance making ctor private
  friend  Derived; // allow inheritance for Derived

public :

  void call ()
  {
      static_cast<Derived *>(this)->call_impl();
  }
};

この後、間違った継承されたD2のインスタンスを作成することは不可能になります。

于 2012-06-28T08:49:32.507 に答える
5

C ++ 11を使用できる場合は、使用できますstatic_assert(使用できない場合は、ブーストを使用してこれらをエミュレートできると確信しています)。たとえばis_convertible<Derived*,Base*>、またはをアサートできis_base_of<Base,Derived>ます。

これはすべてBaseで行われ、Derivedの情報だけがあります。呼び出し元のコンテキストがD2またはD1のどちらからのものであるかを確認する機会はありません。これは、Base<D1>特定の方法で一度インスタンス化されるため、それから派生したD1またはD2のどちらによってインスタンス化されたかに関係なく、違いはありません(またはユーザーが明示的にインスタンス化することによって)。

(当然のことながら、実行時のコストとメモリのオーバーヘッドが大きくなることがあるため)dynamic_castを使用したくないので、「ポリキャスト」と呼ばれるものを使用してみてください(ブーストにも独自のバリアントがあります)。

template<class R, class T>
R poly_cast( T& t )
{
#ifndef NDEBUG
        (void)dynamic_cast<R>(t);
#endif
        return static_cast<R>(t);
}

このようにして、デバッグ/テストビルドでエラーが検出されます。100%の保証ではありませんが、実際には、これは多くの場合、人々が犯すすべての間違いを捕らえます。

于 2012-06-27T11:35:15.527 に答える
2

一般的なポイント:テンプレートは、間違ったパラメーターでインスタンス化されることから保護されていません。これはよく知られている問題です。これを修正するために時間を費やすことはお勧めしません。テンプレートを悪用する方法は無限にあります。あなたの特定のケースでは、あなたは何かを発明するかもしれません。後でコードを変更すると、悪用の新しい方法が表示されます。

C++11には役立つ可能性のある静的アサーションがあることを私は知っています。詳細はわかりません。

その他のポイント。エラーのコンパイルに加えて、静的分析があります。あなたが求めているものは、これに何かを持っています。分析は必ずしもセキュリティ上の欠陥を探すわけではありません。これにより、コードに反省がないことを確認できます。一部のクラスからの派生物がないことを確認したり、テンプレートや関数のパラメータに制限を課したりすることができます。これはすべて分析です。このように大きく変化する制約は、コンパイラーではサポートできません。これが正しい道であるかどうかはわかりませんが、この可能性について話しているだけです。

ps当社はこの分野でサービスを提供しています。

于 2012-06-27T11:25:42.150 に答える
1

ユーザーが誤った派生クラスを作成するのを防ぐ方法はありません。ただし、コードが予期しない階層を持つクラスを呼び出さないようにする方法があります。ユーザーがライブラリ関数に渡すポイントがある場合はDerived、それらのライブラリ関数static_castに期待される派生型に対してを実行させることを検討してください。例えば:

template < typename Derived >
void safe_call( Derived& t )
{
  static_cast< Base< Derived >& >( t ).call();
}

または、階層のレベルが複数ある場合は、次のことを考慮してください。

template < typename Derived,
           typename BaseArg >
void safe_call_helper( Derived& d,
                       Base< BaseArg >& b )
{
   // Verify that Derived does inherit from BaseArg.
   static_cast< BaseArg& >( d ).call();
}

template < typename T >
void safe_call( T& t )
{
  safe_call_helper( t, t );  
}

どちらの場合も、safe_call( d1 )はコンパイルsafe_call( d2 )されますが、コンパイルは失敗します。コンパイラエラーは、ユーザーが望むほど明示的ではない可能性があるため、静的アサートを検討する価値があるかもしれません。

于 2012-06-27T12:14:35.827 に答える
1

C ++ 11で数えられない場合は、次のトリックを試すことができます。

  1. Base特殊な型へのポインタを返す静的関数を追加します。

    static Derived * defined(){return NULL; }

  2. checkポインタを取る静的関数テンプレートをベースに追加します。

    template <typename T> static bool check(T * defined_this){return(derived_this == Base <Derived> ::派生()); }

  3. Dnコンストラクターで、:を呼び出しcheck( this )ます。

    これをチェックして )

今、あなたがコンパイルしようとすると:

$ g++ -Wall check_inherit.cpp -o check_inherit
check_inherit.cpp: In instantiation of ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:
check_inherit.cpp:46:16:   required from here
check_inherit.cpp:19:62: error: comparison between distinct pointer types ‘D2*’ and ‘D1*’ lacks a cast                                                                                                                             
check_inherit.cpp: In static member function ‘static bool Base<Derived>::check(T*) [with T = D2; Derived = D1]’:                                                                                                                   
check_inherit.cpp:20:5: warning: control reaches end of non-void function [-Wreturn-type]                                                                                                                                          
于 2012-06-27T12:02:42.980 に答える
1

一般的に、これを取得する方法はないと思います。これは、完全に醜いと見なされるべきではなく、邪悪な機能の使用に戻ります。これは、機能するものと機能しないものの要約です。

  • の定義のチェックでは型と。しか使用できないため、(static_assertC ++ 11またはboostからの)の使用は機能しません。したがって、次のように見えますが、失敗します。BaseBase<Derived>Derived

    template <typename Derived>
    class Base
    {
       public :
    
       void call ()
       {
          static_assert( sizeof( Derived ) != 0 && std::is_base_of< Base< Derived >, Derived >::value, "Missuse of CRTP" );
          static_cast<Derived *>(this)->call_impl();
       }
    };
    

静的アサーションが実際に派生し 、静的アサーションが完全に有効であるため、静的アサーションがこれをキャッチしないため、D2宣言しようとした場合。ただし、の両方から派生していないクラスがどこから派生した場合でも、コンパイルエラーが発生するため、これはまったく役に立ちません。class D2 : Base< D1 >D1Base< D1 >Base< D3 >D3Base< D3 >static_assertstatic_cast

D2コードをチェックインする必要のある型がBaseテンプレートに渡されることはないため、使用する唯一の方法はstatic_assert、宣言の後にそれを移動することであり、その宣言ではD2、実装した同じ人がD2チェックする必要がありますが、これも役に立ちません。

これを回避する1つの方法は、マクロを追加することですが、これは純粋な醜さしか得られません。

#define MAKE_DISPATCHABLE_BEGIN( DeRiVeD ) \
   class DeRiVeD : Base< DeRiVed > {
#define MAKE_DISPATCHABLE_END( DeRiVeD )
    }; \
    static_assert( is_base_of< Base< Derived >, Derived >::value, "Error" );

これは醜いだけでありstatic_assert、テンプレートはタイプが常に一致することを確認するため、これも不要です。したがって、ここでは利益はありません。

  • 最良のオプション:これをすべて忘れてdynamic_cast、このシナリオで明示的に意図されたものを使用してください。これがより頻繁に必要な場合は、独自に実装するのが理にかなっていますasserted_cast(これについては、Dr。Jobbsに関する記事があります)。これにより、失敗したときにdynamic_cast失敗したアサーションが自動的にトリガーされます。
于 2012-06-27T12:12:32.253 に答える