C++ コア ガイドライン:その他の非規則と神話

C++ で非規則や神話をわかりやすく説明することは、骨の折れる作業ですが、絶対に必要な作業です。目標は単純です。強力なツールである C++ を適切に使用することです。

ところで、私の姓が、特にこの謎解きについて書く資格があることに気付きましたか?とにかく、これが今日の C++ コア ガイドラインのルールです。

  • NR.5:してはいけないこと:コンストラクターで実質的な作業を行わないでください。代わりに 2 フェーズの初期化を使用してください
  • NR.6:してはいけないこと:すべてのクリーンアップ アクションを関数の最後に配置し、goto exit

NR.5:Don' t:コンストラクターで実質的な作業を行わないでください。代わりに 2 フェーズの初期化を使用してください

明らかに、これはコンストラクターの仕事です。コンストラクターが実行された後、完全に初期化されたオブジェクトが必要です。 そのため、ガイドラインの次のコード スニペットは不適切です。

class Picture
{
 int mx;
 int my;
 char * data;
public:
 Picture(int x, int y)
 {
 mx = x,
 my = y;
 data = nullptr;
 }

 ~Picture()
 {
 Cleanup();
 }

 bool Init()
 {
 // invariant checks
 if (mx <= 0 || my <= 0) {
 return false;
 }
 if (data) {
 return false;
 }
 data = (char*) malloc(x*y*sizeof(int));
 return data != nullptr;
 }

 void Cleanup() // (2)
 {
 if (data) free(data);
 data = nullptr;
 }
};

Picture picture(100, 0); // not ready-to-use picture here
// this will fail.. // (1)
if (!picture.Init()) {
 puts("Error, invalid picture");
}
// now have a invalid picture object instance.

ピクチャ (100, 0) は完全に初期化されていないため、ピクチャ インライン (1) に対するすべての操作は無効なピクチャに対して実行されます。この問題の解決策は単純で効果的です:すべての初期化をコンストラクターに入れます。

class Picture
{
 size_t mx;
 size_t my;
 vector<char> data;

 static size_t check_size(size_t s)
 {
 // invariant check
 Expects(s > 0);
 return s;
 }

public:
 // even more better would be a class for a 2D Size as one single parameter
 Picture(size_t x, size_t y)
 : mx(check_size(x))
 , my(check_size(y))
 // now we know x and y have a valid size
 , data(mx * my * sizeof(int)) // will throw std::bad_alloc on error
 {
 // picture is ready-to-use
 }
 // compiler generated dtor does the job. (also see C.21)
};

さらに、データは 2 番目の例では std::vector であり、生のポインターではありません。これは、コンパイラが自動的にクリーンアップするため、最初の例の Cleanup 関数 (2 行目) が不要になったことを意味します。静的関数 check_size のおかげで、コンストラクターはその引数を検証できます。しかし、最新の C++ が放棄する利点はこれで終わりではありません。

多くの場合、コンストラクターを使用して、オブジェクトの既定の動作を設定します。やらないでください。クラス本体でオブジェクトのデフォルトの動作を直接設定します。たとえば、次のクラス Widget と WidgetImpro を比較してください。

// classMemberInitialiserWidget.cpp

#include <iostream>

class Widget{
 public:
 Widget(): width(640), height(480), frame(false), visible(true) {}
 explicit Widget(int w): width(w), height(getHeight(w)), frame(false), visible(true){}
 Widget(int w, int h): width(w), height(h), frame(false), visible(true){}

 void show(){ std::cout << std::boolalpha << width << "x" << height
 << ", frame: " << frame << ", visible: " << visible
 << std::endl;
 }
 private:
 int getHeight(int w){ return w*3/4; }
 int width;
 int height;
 bool frame;
 bool visible;
};

class WidgetImpro{
 public:
 WidgetImpro(){}
 explicit WidgetImpro(int w): width(w), height(getHeight(w)){}
 WidgetImpro(int w, int h): width(w), height(h){}

 void show(){ std::cout << std::boolalpha << width << "x" << height
 << ", frame: " << frame << ", visible: " << visible
 << std::endl;
 }

 private:
 int getHeight(int w){ return w * 3 / 4; }
 int width = 640;
 int height = 480;
 bool frame = false;
 bool visible = true;
};


int main(){

 std::cout << std::endl;

 Widget wVGA;
 Widget wSVGA(800);
 Widget wHD(1280, 720);

 wVGA.show();
 wSVGA.show();
 wHD.show();

 std::cout << std::endl;

 WidgetImpro wImproVGA;
 WidgetImpro wImproSVGA(800);
 WidgetImpro wImproHD(1280, 720);

 wImproVGA.show();
 wImproSVGA.show();
 wImproHD.show();

 std::cout << std::endl;

}

どちらのクラスも同じように動作します。

違いは、クラス WidgetImpro のコンストラクターの方が使いやすく、拡張しやすいことです。両方のクラスに新しい変数を追加すると、WidgetImpro の場合は 1 か所しか編集できませんが、クラス Widget クラスの場合は各コンストラクターが影響を受けます。新しいクラスを設計するときに念頭に置いている図を次に示します。 クラス本体で各オブジェクトのデフォルトの動作を定義します。デフォルトの動作を変更するには、明示的なコンストラクタを使用してください。

終わり?いいえ!

多くの場合、init 関数を使用して、一般的な初期化や検証を 1 か所にまとめます。わかりました。重要な DRY (D Rではありません はいを繰り返す ourself) 原則ですが、コンストラクターの呼び出し後にオブジェクトを完全に初期化する必要があるという他の重要な原則を自動的に破ります。このなぞなぞをどのように解決できますか?結構簡単。 C++11 以降、コンストラクターの委譲があります。これは、一般的な初期化と検証を 1 つのスマート コンストラクターに入れ、他のコンストラクターを一種のラッパー コンストラクターとして使用することを意味します。これが私のアイデアをコードに変換したものです。

// constructorDelegation.cpp

#include <cmath>
#include <iostream>

class Degree{
public:
 explicit Degree(int deg){ // (2)
 degree = deg % 360;
 if (degree < 0) degree += 360;
 }
 
 Degree() = default;
 // (3)
 explicit Degree(double deg):Degree(static_cast<int>(ceil(deg))) {} 

 int getDegree() const { return degree; }

private:
 int degree{}; // (1)
};

int main(){

 std::cout << std::endl;

 Degree degree;
 Degree degree10(10);
 Degree degree45(45);
 Degree degreeMinus315(-315);
 Degree degree405(405);
 Degree degree44(44.45);

 std::cout << "Degree(): " << degree.getDegree() << std::endl;
 std::cout << "Degree(10): " << degree10.getDegree() << std::endl;
 std::cout << "Degree(45): " << degree45.getDegree() << std::endl;
 std::cout << "Degree(-315): " << degreeMinus315.getDegree() << std::endl;
 std::cout << "Degree(405): " << degree405.getDegree() << std::endl;
 std::cout << "Degree(44.45): " << degree44.getDegree() << std::endl;

 std::cout << std::endl;

}

式 int degree{} (行) 1 は、次数を 0 に値初期化します。行 2 のコンストラクターは非常にスマートです。各度を単位円に変換します。 double を取るコンストラクターは、このコンストラクターを使用します。完全を期すために、プログラムの出力を次に示します。

NR.6:Don' t:すべてのクリーンアップ アクションを関数の最後に配置し、goto exit

よし、ガイドラインの次のコードのようにうまくやれる:

void do_something(int n)
{
 if (n < 100) goto exit;
 // ...
 int* p = (int*) malloc(n);
 // ...
exit:
 free(p);
}

ところで。エラーを見つけますか? jump goto exit は、ポインター p の定義をバイパスします。

従来の C コードでよく目にしたのは、このようなコード構造でした。

// lifecycle.c

#include <stdio.h> void initDevice(const char* mess){ printf("\n\nINIT: %s\n",mess); } void work(const char* mess){ printf("WORKING: %s",mess); } void shutDownDevice(const char* mess){ printf("\nSHUT DOWN: %s\n\n",mess); } int main(void){ initDevice("DEVICE 1"); work("DEVICE1"); { initDevice("DEVICE 2"); work("DEVICE2"); shutDownDevice("DEVICE 2"); } work("DEVICE 1"); shutDownDevice("DEVICE 1"); return 0; }

これは非常にエラーが発生しやすいコードですが、典​​型的なコードでもあります。デバイスの各使用は、デバイスの初期化、使用、解放の 3 つのステップで構成されます。正直なところ、これは RAII の仕事です。

// lifecycle.cpp

#include <iostream> #include <string> class Device{ private: const std::string resource; public: Device(const std::string& res):resource(res){ std::cout << "\nINIT: " << resource << ".\n"; } void work() const { std::cout << "WORKING: " << resource << std::endl; } ~Device(){ std::cout << "SHUT DOWN: "<< resource << ".\n\n"; } }; int main(){ Device resGuard1{"DEVICE 1"}; resGuard1.work(); { Device resGuard2{"DEVICE 2"}; resGuard2.work(); } resGuard1.work(); }

コンストラクタでリソースを初期化し、デストラクタで解放します。まず、オブジェクトを初期化することを忘れてはなりません。次に、コンパイラがリソースの解放を処理します。両方のプログラムの出力は同等です:

RAII の詳細については、以前の投稿「C++ コア ガイドライン:RAII が壊れた場合」を参照してください。

その他の神話

これで戦いが終わったわけではなく、C++ に関する非規則や神話についてもご存知でしょう。に手紙を書いてください このメールアドレスはスパムボットから保護されています.表示するには JavaScript を有効にする必要があります。神話を説明し、可能であれば解決策を提示してください。私はあなたのコンテンツから投稿を作成し、気に入った場合はあなたの名前を追加しようとします.あなたのアイデアにとても興味があります。

次のステップ

C++ コア ガイドラインには、非規則および神話に対する 1 つの規則のみが残されています。ご意見をお待ちしております。