コンストラクターと初期化リスト間のパフォーマンスのギャップの比較テスト
1. 説明
C++ のクラスとオブジェクトでは、クラス内のメンバーを初期化するために初期化子リストを使用することが推奨されるということを聞いたことがあるかもしれません。クラス内のメンバーがカスタム型の場合、カスタム型のコンストラクターは初期化リスト内でのみ呼び出すことができます。
しかし、初期化リストとコンストラクター本体での直接代入の間にパフォーマンスの違いはあるのでしょうか? 今日は比較的単純なコードでテストしてみましょう
2. テスト
2.1 コードの説明
まず、デフォルト値によるデフォルト構築、コピー構築、代入オーバーロードの 3 つの機能を実装し、異なる機能を区別するための内部出力を追加したカスタム タイプです。
struct mytest {
public:
mytest(int a = -1)
{
_a = a;
cout << "mytest() " << a << endl;
}
mytest(const mytest& st) {
_a = st._a;
cout << "mytest(copy) " << st._a << endl;
}
mytest& operator=(const mytest& st)
{
_a = st._a;
cout << "mytest operator= " << st._a << endl;
return *this;
}
private:
int _a;
};
次に、このカスタム型を別のクラスで使用します
struct myclass {
public:
myclass(const struct mytest& st, int b)
{
cout << "myclass() _b:" << _b << endl;
}
struct mytest _sa;
int _b;
};
このときコンストラクタの書き方は2通りあり、1つは初期化リストでカスタム型を初期化する方法です。
myclass(const struct mytest& st, int b)
:_sa(st),
_b(b)
{
cout << "myclass() _b:" << _b << endl;
}
もう 1 つは、コンストラクター本体での代入オーバーロードを通じてカスタム型を初期化することです。
myclass(const struct mytest& st, int b)
{
_sa = st;
_b = b;
cout << "myclass() _b:" << _b << endl;
}
ここでのカスタム型パラメータは参照によって渡され、追加のコピーは生成されないことに注意してください。
main 関数本体は次のとおりです。最初のコンストラクターを区別するためにmytest
、その後にセグメンテーションとして出力の行を追加しました。
int main()
{
mytest test_a(1);
cout << "------" << endl;
myclass test(test_a, 3);
return 0;
}
2.2 テスト
まずは代入による初期化の方法を見てみましょう. 初期化リストには何も書いていないにもかかわらず、ここではデフォルトのコンストラクタが呼び出されていることがわかります (デフォルトのコンストラクタのデフォルト値は であるため-1
、ここでは、パラメータ、これは明示的に呼び出された構造ではありません)
デフォルトのコンストラクターを呼び出した後、代入とオーバーロードによって再度初期化されます_sa
。これは 2 回の初期化に相当します。
ただし、初期化リストが呼び出された場合、コピー構築は 1 つだけになり、追加のデフォルト構築呼び出しが回避されます。
Linux でもテストされており、結果は VS2019 と同じです。
3.結論
結論は次のとおりです。初期化リストにより、デフォルトの構築への呼び出しを保存し、パフォーマンスを最適化できます。
3.1 実際のシナリオ
上記のシナリオでは、パフォーマンスのギャップは特に大きくない可能性がありますが、次のシナリオでは異なる可能性があります。
struct mytest {
public:
mytest(int sz = 1)
{
_str = new char[sz];
_sz = sz;
cout << "mytest() " << sz << endl;
}
mytest(const mytest& st) {
delete _str;// 需要先销毁原视的数据
_str = new char[st._sz]; // 再创建一个新的
_sz = st._sz;
//省略拷贝数据的代码
cout << "mytest(copy) " << st._sz << endl;
}
mytest& operator=(const mytest& st)
{
delete _str;// 需要先销毁原视的数据
_str = new char[st._sz]; // 再创建一个新的
//省略拷贝数据的代码
cout << "mytest& operator= " << st._sz << endl;
return *this;
}
private:
char* _str;
size_t _sz;
};
struct myclass {
public:
myclass(const struct mytest& st, int b)
:_sa(st),
_b(b)
{
//_sa = st;
//_b = b;
cout << "myclass() _b:" << _b << endl;
}
struct mytest _sa;
int _b;
};
このシナリオでは、mytest
カスタム タイプのコピー構造にはディープ コピーが含まれるため、既存のスペースを破棄し、新しいスペースを作成してデータをコピーする必要があります。
無駄に、デフォルト構造内に新しいスペースの層が追加され、コピー構造内の削除で元のスペースが消費されます。
ディープコピーを必要とするクラスに複数のメンバーがいる場合、パフォーマンスの差はさらに大きくなります。
したがって、C++ では、初期化リストが常に優先されます。
ところで、これは初期化リストの小さな落とし穴です。復習とも言えます。
初期化リストを使用してクラス内のメンバーを初期化する場合、初期化の順序は、初期化リスト内の順序ではなく、クラス内でメンバーが宣言された順序になります。これは非常に重要で、順序が間違っていると、未定義 (初期化されていない) パラメーターを使用してバグが発生する可能性があります。