Imperfect C++読書ノート(三)

13441 ワード

配列とポインタ
まず古典的なC/C++配列要素の個数を求める解決策を見てみましょう.
#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof((x)[0]))

C/C++コンパイラを使用して式ar[n]をコンパイラで*(ar+n)として解釈すると、より先進的なバージョンを提供できます.
#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof(0[(x)]))

このような下付きインデックスの交換性は、内蔵の下付きインデックスオペレータにのみ有効であり、この制限はNUM_を制約するために使用することができる.MELEMENTTSマクロは配列/ポインタにのみ有効であり、下付きインデックスオペレータのクラスタイプの再ロードを拒否します.
 
次にNUM_を見てみましょうMELEMENTSの実用化:
int ar[10];
cout << NUM_ELEMENTS(ar) << endl;        //     ,   10。
...
void fn(int ar[10])
{
    cout << NUM_ELEMENTS(ar) << endl;    //      ?    10,          ,
}                                        //    ,     1。

すべてがきちんとしているように見えますが、問題はいったいどこにあるのでしょうか.ええ、そうです.C/C++では配列を関数に渡すことはできません.Cでは、配列が関数を渡すときにポインタに変換され、配列サイズを取得しようとする企みをあっさりと阻止します.C++は互換性の考慮に基づいて、同様である.
だから先に与えられたNUM_ELEMENTTSマクロ定義は、プリプロセッサによるテキスト置換に依存するため、ポインタに適用すると、テキスト置換の結果が間違ってしまうという深刻な欠陥があります.
 
幸いなことに、多くの現代のコンパイラがサポートしている特性を利用して、配列とポインタを区別することができます.これは、配列からポインタへの変換(劣化)が参照タイプのテンプレートの実パラメータ決定では発生しません.したがって、NUM_ELEMENTSマクロを再定義することができます.
template<int N>
struct array_size_struct
{
    byte_t c[N];
};
 
template<typename T, int N>
array_size_struct<N> static_array_size_fn(T(&)[N]);
 
#define NUM_ELEMENTS(x) sizeof(static_array_size_fn(x).c)

基本原理は、要素タイプT、サイズNの配列の参照を受け入れるテンプレート関数を宣言(定義しない)します.これにより、ポインタタイプおよびユーザー定義タイプが拒否されます(コンパイルエラー).また、C++規格ではsizeof()のオペランドが評価されないため、static_を定義する必要はありません.array_size_fn()関数体は、上記の施設が完全にゼロの代価であるようにする.実行時のオーバーヘッドがなく、コードが膨張することもありません.
 
「C/C++配列は関数に渡されるとポインタに劣化する」という問題に戻りましょう.もし私たちが現実的に任意の長さの配列を所望の受け入れ配列の関数に渡す必要がある場合、どうすればいいのでしょうか.困惑の本質は,配列の大きさが伝達過程で失われたことであるため,配列の大きさを関数に伝達するメカニズムを見つけることができれば,問題は解決される.上記のマクロ定義の経験から、テンプレートを使用して解決策を見つけました.
template<typename T>
class array_proxy
{
public:
    typedef T               value_type;
    typedef array_proxy<T>  class_type;
    typedef value_type *    pointer;
    typedef value_type *    const_pointer;      // Non-const!
    typedef value_type &    reference;
    typedef value_type &    const_reference;    // Non-const!
    typedef size_t          size_type;
//     
public:
    template<size_t N>
    explicit array_proxy(T(&t)[N])    //      T   
        : m_begin(&t[0])
        , m_end(&t[N])
    {}
    template<typename D, size_t N>
    explicit array_proxy(D(&d)[N])    //      T       
        : m_begin(&d[0])
        , m_end(&d[N])
    {
        constraint_must_be_same_size(T, D);    //   D T    
    }
    template<typename D>
    array_proxy(array_proxy<D> &d)
        : m_begin(d.begin())
        , m_end(d.end())
    {
        constraint_must_be_same_size(T, D);    //   D T    
    }
//   
public:
    pointer             base();
    const_pointer       base() const;
    size_type           size() const;
    bool                empty() const;
    static size_type    max_size();
//        
public:
    reference        operator [](size_t index);
    const_reference  operator [](size_t index) const;
//     
public:
    pointer          begin();
    const_pointer    begin() const;
    pointer          end();
    const_pointer    end() const;
//     
private:
    pointer const m_begin;
    pointer const m_end;
//        
private:
    array_proxy & operator =(array_proxy const &);
};
 
//     
template<typename T, size_t N>
inline array_proxy<T> make_array_proxy(T(&t)[N])
{
    return array_proxy<T>(t);
}
template<typename T>
inline array_proxy<T> make_array_proxy(T * base, size_t size)
{
    return array_proxy<T>(base, size);
}

顧客コードを次のように変更します.
void process_array(const array_proxy<int> & ar)
{
    std::copy(ar.begin(), ar.end(), ostream_iterator<int>(cout, " "));
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    int ar[5] = {0, 1, 2, 3, 4};
    
    process_array(make_array_proxy(ar));
 
    return 0;
}

私たちの問題はついに徹底的な解決策を得た.このソリューションは効率的であり(従来のコンパイラでは追加のオーバーヘッドはありません)、タイプが安全であり、関数の設計者が潜在的な誤用を防止することができます.(より正確には、コードが派生クラスの配列の誤用をより強く防ぐことができます).また、派生クラスが親クラスと同じサイズの場合、それらの配列が「エージェント」されることを可能にする十分な知能もあります.
最後の利点は、誤った配列長を変調関数に伝達することはもはや不可能であることであり、以前は2つのパラメータ(1つの伝達配列ポインタ、1つの伝達配列長)を用いた関数バージョンでは、長さを誤伝達する危険が時々存在していた.この優位性はDRY(Don't Repeat Yourself!)の原則に従うことができる.
 
NULLマクロ
C言語ではvoid*タイプを暗黙的に他の任意のポインタタイプに変換できるので、NULLを(void*)0)と定義して、他の任意のポインタタイプとの相互変換を実現することができます.しかしながら、C++は、void*から任意のポインタへの暗黙的な変換を許さず、また、C++中の0が任意のポインタタイプに変換できるため、C++標準では、NULLマクロは実装によって定義されたC++空のポインタ定数である...その可能な定義方法は0と0 Lを含むが、決して(void*)0ではない.
0は疑いの余地なく任意の整数型に変換できるため、wchar_tとbool、および浮動小数点タイプは、NULLを使用すると、タイプチェックが発生しなくなり、私たちは気づかずに厄の淵に向かいやすく、警告もありません.次の状況を考慮します.
//         
//
class String
{
    explicit String(char const *s);                  //           
    explicit String(int cch, char chInit = '\0');    //              ,        
};

NULLをパラメータとしてStringを構築すると、2番目のコンストラクション関数が呼び出されます!あなたの初心とは違うかもしれませんが、コンパイラは黙ってコンパイルします.これはまずい.intをsizeに変更するとt(またはshort、またはlong、またはint以外の組み込みタイプ)では、コンパイラは2つの変換の間で左右に困ってしまい、結果として2つの意味的なエラーが得られます.
 
完璧な空のポインタキーがほしいです!すぐに著者は方法を考え出しました.驚くべきではありません.解決策はテンプレートから離れられません.
struct NULL_v
{
//     
public:
    NULL_v()
    {}
//      
public:
    template<typename T>
    operator T* () const
    {
        return 0;
    }
    template<typename T2, typename C>
    operator T2 C::*() const
    {
        return 0;
    }
    template<typename T>
    bool equals(T const & rhs) const
    {
        return rhs == 0;
    }
//        
private:
    void operator &() const;    // Scott:                   。
    NULL_v(NULL_v const &);
    NULL_v& operator =(NULL_v const &);
};
 
template<typename T>
inline bool operator ==(NULL_v const & lhs, T const & rhs)
{
    return lhs.equals(rhs);
}
 
template<typename T>
inline bool operator ==(T const & lhs, NULL_v const & rhs)
{
    return rhs.equals(lhs);
}
 
template<typename T>
inline bool operator !=(NULL_v const & lhs, T const & rhs)
{
    return !lhs.equals(rhs);
}
 
template<typename T>
inline bool operator !=(T const & lhs, NULL_v const & rhs)
{
    return !rhs.equals(lhs);
}