C++にSwap関数を書く

6444 ワード

swap関数はほとんどの初心者が書いた最も基本的な関数の一つであり、通常は関数概念、パラメータと実パラメータ、参照、ポインタを理解するために使用されます.しかし,この極めて基礎的な関数は非常に重要な役割を果たしている.その重要性,普遍性こそが,swap関数に極めて高い安全性が必要であることを意味するので,異常を投げ出さないswap関数をどのように書くかが本章で議論するテーマである.
一般的なSwap関数
まず例を見てみましょう
void swap(int a, int b)
{
    int swap = a;
    a = b;
    b = swap;
}
これは最も一般的なエラーであり、aおよびbintタイプの値であり、それらは実パラメータのコピーにすぎず、役割ドメインはswap関数内にのみ存在するため、交換の役割を果たすことができない.
これは最も一般的なswap関数の実装です.
void swap(int& a, int& b)
{
    int swap = a;
    a = b;
    b = swap;
}
参照を使用することによって、swap関数におけるパラメータの変更がswapの役割ドメイン外に伝達されることを保証する.
もう一つの方法は
void swap(int& a, int& b)
{
    a = a + b;   // a + b
    b = a - b;   // a + b - b = a
    a = a - b;   // a + b - a = b
}
この関数は,abの一連の演算により,中間容器としての新しい変数の申請を回避し,空間を節約した.しかし、このようなやり方は本当に損得がありません.その欠点が多すぎるからです.
  • は、加算減算が使用されているため、組み込みタイプにのみ適用されます.この方法は、クラスタイプの変数には使用できません(クラスが+-演算子を再ロードしない限り).
  • 省スペースは卵がないと言えます.この方法は一般的に内蔵タイプにしか使われていませんが、現在のコンピュータの多くは64ビットで、内蔵タイプは最大8バイトを占めています.このメモリは現在の機械にとって何でもありません.3つの加減演算で置き換えられ、損をしません.
  • でデータオーバーフローや精度損失が発生する可能性があります.栗:int型変数の範囲は-2^31~2^31-1です.パラメータがint型の場合、abが2^31-1の場合、最初のステップa = a + bでデータオーバーフローが発生します.パラメータのタイプがfloatまたはdoubleの場合、基本演算を行うと精度の損失が発生します.
  • 上記のswap関数の別のバージョン:
    void swap(int* a, int* b)
    {
        *a = *a + *b;
        *b = *a - *b;
        *a = *a - *b;
    }
    
    これは、上記の3つの欠点に加えて、ポインタabが同じアドレス、すなわち別名を使用した場合、*aが元の2倍になり、*bが0に等しいという欠点がある.
    これは、ビット演算によってswap関数を実現し、追加の記憶空間を必要としない方法です.
    void swap(int a, int b)
    {
        a = a ^ b;      // a ^ b
        b = a ^ b;      // a ^ b ^ b = a
        a = a ^ b;      // a ^ b ^ a = b
    }
    
    クラスとコンテナのswap関数
    標準ライブラリにはデフォルトのswap関数が用意されており、予想通りに実現されています.
    namespace std {
        template 
        void swap ( T& a, T& b )
        {
            T c(a);
            a = b;
            b = c;
        }
    }
    
    Tがcopyコンストラクション関数を持つ限り,このデフォルトのswap関数は動作する.しかし、小川を流すのを助けることができますが、太平洋を越えるには、少し足りません.オブジェクトに大量のデータが含まれている場合、この方法は3回の長時間の付与演算を行い、特に置換されたクラスがpimplの手法で実現された場合、効率が極めて低下する.
    注:pimpl手法(pointer to implementation)は、オブジェクト実装の詳細をプライベートポインタメンバーによって隠す一般的な手法であり、インタフェースと実装を分離し、それらの間の結合関係を解除し、ファイル間のコンパイル依存性を低減することができる.
    pimpl手法の栗:
    class BoxImpl
    {
    public:                 //      
        ...
    private:
        string name;            //       
        string id;
        std::vector vec;
        ...
    };
    
    class Box
    {
    public:
        Box(const Box& x);          // copy    
        Box& operator=(const Box& x)
        {
            ...
            *pImpl = *(x.pImpl);        //   '='   ,  Box   
            ...
        }
        ...
    private:
        BoxImpl* pImpl;             //   Box   
    };
    
    私たちが考えているように、2つのBoxオブジェクトを置き換えると、3つのBoxをコピーする以外に、3つのBoxImplをコピーするので、swap関数を実現する別の方法が必要です.特化バージョンのstd::swap関数を生成してpImplポインタだけを置き換えると(stdネーミングスペースの何も変更できませんが、標準テンプレートに特化バージョンを追加できます)、swap関数を呼び出すときにBoxオブジェクトにカスタマイズされた方法を使用します.他のオブジェクトに対してデフォルトのメソッドを使用します.
    namespace std {
        template<>
        void swap(Box& a, Box& b)
        {
            swap(a.pImpl, b.pImpl);
        }
    }
    
    しかし、この関数はコンパイルできません.pImplポインタはBoxのプライベートメンバーなので、Boxクラスにpublicのインタフェースを追加してプライベートメンバーの呼び出しを隠す必要があります.
    class Box {
    public:
        ...
        void swap(Box& x)
        {
            using std::swap;        //           
            swap(pImpl, x.pImpl);       //   pImpl  
        }
        ...
    };
    
    namespace std {
        template<>
        void swap(Box& a, Box& b)
        {
            a.swap(b);
        }
    }
    
    このアプローチは、STLコンテナによるswapの実装と一致し、STLコンテナはstd::swapの特化版によってコンテナのpublic swapメンバー関数を呼び出し、このメンバー関数はデータを置換するポインタにすぎない.
    目標を達成したが、classではなくclass templatesを置き換えると、swap関数が足りないことが分かった.読者はうんざりしているかもしれませんが、私たちは小さなswap関数を実現しただけで、どうしてこんなに多くのものを互換性があるのですか!!!あなたは変わって、あなたはもう昔の純潔で素朴なswap関数ではありません(TДT)しかし、効率のためにこのようにするのは価値がある...std::swapを偏特化しました
    namespace std {
        template    
        void swap< Box >(Box& a, Box& b)
        {
            a.swap(b);          //   !          。
        }
    }
    
    これは合理的に見えるが,C++はクラステンプレートの偏特化のみを可能にし,関数テンプレートの偏特化操作Orzをサポートしない.そこで賢いあなたはまた良いアイデアを思いつき、swap関数を再ロードしました.
    namespace std {
        template
        void swap(Box& a, Box& b)
        {
            a.swap(b);          //     !
        }
    }
    
    ここで発生した問題は、stdネーミングスペースに特化したstdテンプレートを追加することができ、新しいテンプレートを追加することはできません.
    注意:これもプログラムをコンパイルして実行する場合がありますが、それらの動作は明確に定義されていませんので、stdに新しいものを追加しないでください.
    クラステンプレートのswapメンバー関数を呼び出すために非メンバーswap関数を宣言することができますが、std::swap関数の特化またはリロードバージョンとして宣言することはありません.Boxに関連するすべての機能をネーミングスペースBoxToolに配置します(globalネーミングスペースにも配置できますが、多くのクラス、テンプレート、方法などがあなたのglobalネーミングスペースにあふれていると、コードが優雅ではありません):
    namespace BoxTool {
        ...                 //     BoxImpl Box    
        template
        void swap(Box& a, Box& b)
        {
            a.swap(b);          //       std     
        }                   //     std::swap        
    }
    
    そこで私たちは安心してBoxオブジェクトを置き換えることができます.しかし、この方法はクラスにも当てはまるようなので、ずっとこの方法を使うほうが便利ではないでしょうか.しかし、そうではない.クラス特化されたswap関数をできるだけ多くの文脈で呼び出すには、クラスが存在するネーミングスペースに非メンバーのswap関数とstd::swap関数を追加する必要があります.
    さあ、やっとswap関数を書きました.私たちはそれを使うことができます\(>0<)/:
    template
    void doSomething(T& a, T& b)
    {
        ...
        swap(a, b);
        ...
    }
    
    しかしswapはどのバージョンを呼び出しますか?stdの一般化バージョン?特化std::swapバージョン?クラスが存在するネーミングスペースのswapバージョン?呆然とした(◎_◎;)Tの専属バージョンをオンにし、存在しない場合は一般的なstd::swap関数を呼び出します.
    template
    void doSomething(T& a, T& b)
    {
        using std::swap;        //  std::swap      
        ...
        swap(a, b);         //     :std::swap(a, b);
        ...
    }
    
    C++の名前でルールを検索することで、私たちが使用しているswap関数が最も適切であることを確認します.しかし、コンパイラはやはり自分の人が好きなので、Tがいるネーミングスペースのテンプレート関数ではなくstdの特化バージョンを使用することに偏っています.
    こんなにたくさんのswap関数の実現を議論して、自分のために適当なswap関数を見つけましょう(●'◡'●)ノ♥
    本文は『Effective C++』第25条から参考する