C++ コア ガイドライン:比較、スワップ、およびハッシュ

この投稿では、比較、スワップ、およびハッシュについて説明します。つまり、C++ の既定の操作規則に関する彼の投稿で締めくくります。

これが 9 つのルールです。

  • C.80:22 を使用 デフォルトのセマンティクスの使用について明示する必要がある場合
  • C.81:37 を使用 デフォルトの動作を無効にしたい場合 (別の方法は必要ありません)
  • C.82:コンストラクタとデストラクタで仮想関数を呼び出さない
  • C.83:値のような型については、44 を提供することを検討してください スワップ機能
  • C.84:54 失敗しないかもしれません
  • C.85:63 にする 76
  • C.86:86 にする オペランドの型と 90 に関して対称
  • C.87:104 に注意してください 基本クラスについて
  • C.89:119 を作る 120

詳細を見てみましょう。

デフォルトの運用ルール:

C.80:139 を使用 デフォルトのセマンティクスの使用について明示する必要がある場合

5の法則を覚えていますか?つまり、5 つの特別なメソッドのいずれかを定義すると、それらすべてを定義する必要があります。

ここがポイントです。

次の例のようにデストラクタを実装する場合、コピーと移動のコンストラクタと代入演算子を定義する必要があります。

class Tracer {
 string message;
public:
 Tracer(const string& m) : message{m} { cerr << "entering " << message << '\n'; }
 ~Tracer() { cerr << "exiting " << message << '\n'; }

 Tracer(const Tracer&) = default;
 Tracer& operator=(const Tracer&) = default;
 Tracer(Tracer&&) = default;
 Tracer& operator=(Tracer&&) = default;
};

それは簡単でした!右?しかし、私は自分でそれを行うこともできますが、これは少なくとも退屈ですが、エラーが発生しやすくなります.

class Tracer2 {
 string message;
public:
 Tracer2(const string& m) : message{m} { cerr << "entering " << message << '\n'; }
 ~Tracer2() { cerr << "exiting " << message << '\n'; }

 Tracer2(const Tracer2& a) : message{a.message} {}
 Tracer2& operator=(const Tracer2& a) { message = a.message; return *this; }
 Tracer2(Tracer2&& a) :message{a.message} {}
 Tracer2& operator=(Tracer2&& a) { message = a.message; return *this; }
};

C.81:142 を使用 デフォルトの動作を無効にしたい場合 (別の方法は必要ありません)

場合によっては、デフォルトの操作を無効にしたいことがあります。ここで削除がプレイに登場します。 C++ は独自のドッグフードを食べます。 lock、mutex、promise、future などの型のコピー コンストラクターは、delete に設定されています。スマート ポインター std::unique_ptr:std::unique_ptr(const std::unique_ptr&) =delete についても同じことが言えます。

delete を使用して、ストレンジ型を作成できます。イモータルのインスタンスは破壊できません。

class Immortal {
public:
 ~Immortal() = delete; // do not allow destruction
 // ...
};

void use()
{
 Immortal ugh; // error: ugh cannot be destroyed
 Immortal* p = new Immortal{};
 delete p; // error: cannot destroy *p
}

C.82:コンストラクタとデストラクタで仮想関数を呼び出さない

このルールは、ルール C.50:初期化中に「仮想動作」が必要な場合はファクトリ関数を使用するに非常に似ています。これについては、C++ コア ガイドライン:コンストラクターの投稿で説明しました。

次の 3 つのルールは、swap 関数に関するものです。一緒にやりましょう。

C.83:値のような型については、152 を提供することを検討してください スワップ関数、C.84:A 165 C.85:Make 179 189

スワップ機能はとても便利です。

template< typename T >
void std::swap(T & a, T & b) noexcept {
 T tmp(std::move(a));
 a = std::move(b);
 b = std::move(tmp);
}

C++ 標準では、std::swap に対して 40 を超える特殊化が提供されています。コピー構築/代入など、多くのイディオムのビルディング ブロックとして使用できます。スワップ関数は失敗してはなりません。したがって、noexcept として宣言する必要があります。

std::swap を使用した移動代入操作の例を次に示します。 pdata は配列を指します。

class Cont{ 
public:
 Cont& operator=(Cont&& rhs);
 
private:
 int *pData; 
};

Cont& Cont::operator=(Cont&& rhs){
 std::swap(pData, rhs.pData);
 return *this;
}

C.86:191 にする オペランドの型と 203 に関して対称

ユーザーを驚かせたくない場合は、==演算子を対称にする必要があります。

これは、クラス内で定義されている直観的でない ==演算子です。

class MyNumber {
 int num;
public:
 MyNumber(int n): num(n){};
 bool operator==(const MyNumber& rhs) const { return num == rhs.num; }
};

int main(){
 MyNumber(5) == 5;
 // 5 == MyNumber(5);
}

MyNumber(5) ==5 の呼び出しは、コンストラクターが int 引数を MyNumber のインスタンスに変換するため有効です。最後の行でエラーが発生します。自然数の比較演算子は、MyNumber のインスタンスを受け入れません。

この非対称性を解決するエレガントな方法は、フレンド 212 を宣言することです クラス MyNumber 内。これが MyNumber の 2 番目のバージョンです。

class MyNumber {
 int num;
public:
 MyNumber(int n): num(n){};
 bool operator==(const MyNumber& rhs) const { return num == rhs.num; }
 friend bool operator==(const int& lhs, const MyNumber& rhs){ 
 return lhs == rhs.num; 
 }
};

int main(){
 MyNumber(5) == 5;
 5 == MyNumber(5);
}

驚きは続きます。

C.87:228 に注意してください 基本クラス

階層のための誰にでもできる ==演算子を書くのは難しいです。ガイドラインは、そのような大変な仕事の良い例を示しています。これが階層です。

class B {
 string name;
 int number;
 virtual bool operator==(const B& a) const
 {
 return name == a.name && number == a.number;
 }
 // ...
};

class D :B {
 char character;
 virtual bool operator==(const D& a) const
 {
 return name == a.name && number == a.number && character == a.character;
 }
 // ...
};

試してみましょう。

B b = ...
D d = ...
b == d; // compares name and number, ignores d's character // (1)
d == b; // error: no == defined // (2) 
D d2;
d == d2; // compares, name, number, and character
B& b2 = d2;
b2 == d; // compares name and number, ignores d2's and d's character // (1)

B のインスタンスまたは D のインスタンスを比較するとうまくいきます。ただし、B と D のインスタンスを混在させると、期待どおりに動作しません。 B の ==演算子を使用すると、D の文字 (1) が無視されます。 D の演算子を使用しても、B のインスタンスでは機能しません (3)。最後の行はかなりトリッキーです。 B の ==演算子が使用されます。なんで? D の ==演算子は、B の ==演算子を上書きしました。本当ですか?いいえ!どちらのオペレーターも異なる署名を持っています。 B のインスタンスを取るもの。もう一方は D のインスタンスを取得します。D のバージョンは B のバージョンを上書きしません。

この観察は、他の 5 つの比較演算子 (!=、<、<=、>、および>=) にも当てはまります。

C.89:231 を作る 244

ハッシュ関数は、std::unordered_map などの順序付けられていない連想コンテナーによって暗黙的に使用されます。ユーザーは、投げることを期待していません。順序付けられていない連想コンテナーで独自の型をキーとして使用する場合は、キーのハッシュ関数を定義する必要があります。

クラスの属性に std::hash 関数を使用し、それらを ^ (xor) で結合して実行します。

struct MyKey{
 int valInt = 5;
 double valDou = 5.5;
};

struct MyHash{
 std::size_t operator()(MyKey m) const {
 std::hash<int> hashVal1;
 std::hash<double> hashVal2;
 return hashVal1(m.valInt) ^ hashVal2(m.valDou);
 }
};

次は?

ガイドラインに従って、次のトピックはコンテナーとその他のリソース ハンドルである必要がありますが、ルールの名前しか使用できません。したがって、この部分はスキップして、次の投稿でラムダ式に直行します。