C++ コア ガイドライン:禁止事項に関する規則

この投稿は、禁止事項に関するものです。この投稿で最も重要な 2 つのルールは次のとおりです。始めましょう。

今日の禁止事項は次のとおりです。

  • ES.56:std::move() と書く オブジェクトを別のスコープに明示的に移動する必要がある場合のみ
  • ES.60:new を避ける と delete 外部リソース管理機能
  • ES.61:delete[] を使用して配列を削除する および delete を使用する非配列
  • ES.63:スライスしないでください

最初のルールは偽装禁止です。

ES.56:Write std::move() オブジェクトを別のスコープに明示的に移動する必要がある場合のみ

ほとんどの場合、明示的に std::move を呼び出す必要はありません。操作のソースが右辺値である場合、コンパイラは自動的に移動セマンティックを適用します。右辺値は、ID を持たないオブジェクトです。通常、右辺値には名前がなく、そのアドレスを取得することはできません。残りのオブジェクトは左辺値です。

std::move を左辺値に適用すると、ほとんどの場合空のオブジェクトが得られます。その後、左辺値はいわゆる移動元の状態になります。これは、有効ではあるが指定された状態に近いものではないことを意味します。奇妙に聞こえますか?右!このルールを覚えておいてください:std::move(source) のような左辺値から移動した後は、ソースについて何の仮定もできません。新しい値に設定する必要があります。

一瞬待って。この規則では、オブジェクトを別のスコープに移動する場合にのみ std::move を使用する必要があると規定されています。古典的なユースケースは、コピーできないが移動できるオブジェクトです。たとえば、std::promise を別のスレッドに移動するとします。

// moveExplicit.cpp

#include <future>
#include <iostream>
#include <thread>
#include <utility>

void product(std::promise<int>&& intPromise, int a, int b){ // (1)
 intPromise.set_value(a * b);
}

int main(){

 int a= 20;
 int b= 10;

 // define the promises
 std::promise<int> prodPromise;

 // get the futures
 std::future<int> prodResult= prodPromise.get_future();

 // calculate the result in a separat thread
 std::thread prodThread(product,std::move(prodPromise), a, b); // (2)
 
 // get the result
 std::cout << "20 * 10 = " << prodResult.get() << std::endl; // 200
 
 prodThread.join();

}

関数 product (1) は、右辺値参照によって std::promise を取得します。 promise はコピーすることはできませんが、移動することはできません。したがって、新しく作成されたスレッドにプロミスを移動するには、std::move が必要です (2)。

これが大きな禁止事項です! return ステートメントで std::move を使用しないでください。

vector<int> make_vector() {
 vector<int> result;
 // ... load result with data
 return std::move(result); // bad; just write "return result;"
}

あなたのオプティマイザーを信頼してください!コピーだけでオブジェクトを返すと、オプティマイザーがその仕事をします。これは C++14 までのベスト プラクティスです。これは C++17 以降の必須の規則であり、保証されたコピー省略と呼ばれます。この手法は自動コピー省略と呼ばれますが、C++11 では移動操作も最適化されています。

RVO R の略 V を返す 最適化 また、コンパイラが不要なコピー操作を削除できることを意味します。 C++14 まで可能だった最適化ステップが、C++17 では保証されます。

MyType func(){
 return MyType{}; // (1) no copy with C++17
}
MyType myType = func(); // (2) no copy with C++17

これらの数行で 2 つの不要なコピー操作が発生する可能性があります。 (1) の最初のものと (2) の 2 番目のもの。 C++17 では、両方のコピー操作は許可されていません。

戻り値に名前がある場合は、NRVO と呼ばれます。 この頭字語は Nの略です アメッド R V を返す アルエ O 最適化。

MyType func(){
 MyType myVal;
 return myVal; // (1) one copy allowed 
}
MyType myType = func(); // (2) no copy with C++17

微妙な違いは、コンパイラが C++17 (1) に従って値 myValue を引き続きコピーできることです。ただし、(2) ではコピーは行われません。

ES.60:new を避ける と delete 外部リソース管理機能

わかりました、私はそれを短くすることができます。 new を使用せず、アプリケーション コードを削除します。このルールには、「裸の新しいものはありません!」という良い思い出があります。

ES.61:delete[] を使用して配列を削除する および delete を使用する非配列

最後のルールの根拠は次のとおりです。アプリケーション コードでのリソース管理はエラーが発生しやすいです。

void f(int n)
{
 auto p = new X[n]; // n default constructed Xs
 // ...
 delete p; // error: just delete the object p, rather than delete the array p[]
}

ガイドラインのコメントには、「オブジェクト p を削除するだけ」と記載されています。もっと極端に言えば。これは未定義の動作です!

ES.63:スライスしない

初めに。スライスとは?スライスとは、代入または初期化中にオブジェクトをコピーしたい場合に、オブジェクトの一部のみを取得することを意味します。

簡単に始めましょう。

// slice.cpp

struct Base { 
 int base{1998};
}
 
struct Derived : Base { 
 int derived{2011};
}

void needB(Base b){
 // ...
}
 
int main(){

 Derived d;
 Base b = d; // (1)
 Base b2(d); // (2)
 needB(d); // (3)

}

行 (1)、(2)、および (3) はすべて同じ効果があります。d の派生部分が削除されます。それはあなたの意図ではなかったと思います.

この記事の発表で、スライシングは C++ の最も暗い部分の 1 つだと言いました。暗くなりました。

// sliceVirtuality.cpp

#include <iostream>
#include <string>

struct Base { 
 virtual std::string getName() const { // (1)
 return "Base"; 
 }
};
 
struct Derived : Base { 
 std::string getName() const override { // (2)
 return "Derived";
 }
};
 
int main(){
 
 std::cout << std::endl;
 
 Base b;
 std::cout << "b.getName(): " << b.getName() << std::endl; // (3)
 
 Derived d;
 std::cout << "d.getName(): " << d.getName() << std::endl; // (4)
 
 Base b1 = d;
 std::cout << "b1.getName(): " << b1.getName() << std::endl; // (5)
 
 Base& b2 = d;
 std::cout << "b2.getName(): " << b2.getName() << std::endl; // (6)

 Base* b3 = new Derived;
 std::cout << "b3->getName(): " << b3->getName() << std::endl; // (7)
 
 std::cout << std::endl;

}

Base クラスと Derived クラスで構成される小さな階層を作成しました。このクラス階層の各オブジェクトは、その名前を返す必要があります。メソッド getName virtual (1) を作成し、(2) でオーバーライドしました。したがって、私はポリモーフィズムを持ちます。これは、参照 (6) または基本オブジェクトへのポインター (7) を介して派生オブジェクトを使用できることを意味します。内部的には、オブジェクトは Derived 型です。

Derived d を Base b1 (5) にコピーするだけでは、これは成立しません。この場合、スライスが開始され、フードの下に Base オブジェクトがあります。コピーの場合は、宣言型または静的型が使用されます。参照やポインターなどの間接参照を使用する場合は、実際の型または動的型が使用されます。

ルールを覚えておくのは非常に簡単です。クラスのインスタンスがポリモーフィックである必要がある場合は、少なくとも 1 つの仮想メソッドを宣言または継承する必要があり、ポインターや参照などの間接化を介してそのオブジェクトを使用する必要があります。

もちろん、スライシングを解決する方法はあります。仮想クローン機能を提供することです。詳細については、C++ コア ガイドライン:コピーと移動のルールをご覧ください。

次のステップ

この投稿は、禁止事項に関するものでした。次の投稿はドから始まります。データの初期化には中かっこを使用します。