初期化とは何ですか
変数のタイプによって、変数が占有するメモリのサイズとレイアウトが決まります。変数は作成時に特定の値を取得し、変数が初期化されると言います。
分類と規格
変数の初期化は C++ 標準の重要な部分です。C++ の初期化は、コピーの有無によって直接初期化とコピー初期化に分けられ、その他の初期化はこれら 2 つのカテゴリに分類できます。
C++ 標準では、代入演算子 (=) を使用して変数を初期化する場合はコピー初期化、それ以外の場合は直接初期化と規定しています。
C++標準
C++ 標準における時間の導入に従って、著者は変数初期化の関連標準を次のように要約しています。
- C++98 標準の直接初期化、コピー初期化、集合体の初期化、括弧の初期化
- C++11標準リストの初期化とinitializer_list;
- C++17 標準のコピー最適化の必須省略、リスト初期化型推定、集合体初期化拡張 (基本クラスを許可)
- 集約初期化、指定イニシャライザー初期化のための C++20 標準の拡張。
C++98
C++98 標準は、C++ 言語の最初のメジャー バージョンです。この標準によって導入された初期化には、直接初期化、コピー初期化、括弧初期化、中括弧、および集計初期化が含まれます。
前述の初期化標準によれば、括弧初期化は直接初期化に属し、集約初期化は特殊な種類のコピー初期化です。
直接初期化
コピー構築を介さない初期化は直接初期化であり、通常はコンストラクターを直接呼び出して変数を初期化します。例えば:
string s(5,'c'); // s被初始化为"ccccc"
コピーの初期化
コピー初期化は直接初期化に関連しており、型のコピー構築を呼び出すことによってオブジェクトの初期化を実装します。例えば:
string name = "liuguang";
C++98 標準では、名前の割り当ては暗黙の変換に基づいて 2 つの手順を経ます。
- 文字列入力パラメーターを const char* コンストラクターとして呼び出して、一時文字列変数 temp を生成します。
- 代入操作を実行し、入力パラメータが temp である文字列コピー コンストラクターを呼び出して、name の初期化を完了します。
しかし、コンパイラの最適化により、コンストラクターの初期化が直接呼び出され、名前の初期化が文字列入力パラメーターを const char* コンストラクターとして直接呼び出すことがデバッグで確認されたのは残念です。
したがって、名前初期化はコピー初期化に属するはずですが、最適化後は他の人からは直接初期化と誤解されてしまいます。GCC の -fno-elide-constructors と MSVC の /Od は、このコンパイラの最適化をオフにすることができます。
括弧の初期化
括弧の初期化では、() を使用して変数を初期化します。変数は、使用形式の命名方法に基づいています。次に例を示します。
double d(1.3); // d被初始化为1.3
string s(5,'c'); // s被初始化为"ccccc"
集約の初期化
集約の初期化は通常、次の 2 つの初期化シナリオに適用できます。
- 配列の初期化
char a[3] = {
'a', 'b', '\0'};
- 次の条件を満たすクラス、構造体、または共用体
- すべてのメンバーがパブリックな非静的データ メンバーである
- ユーザー定義コンストラクター
なし - 仮想メンバー関数なし
- 基本クラスなし
- クラス内初期化なし
struct AggrPerson
{
std::string name;
int age;
};
AggrPerson aggrPerson = {
"liuguang", 20};
中括弧の初期化
C++98 標準では、中括弧の初期化には、集合体初期化以外の使用シナリオが 1 つだけあります。例えば
int units{
2};
C++11
C++11 標準は C++ 言語の 2 番目のメジャー バージョンであり、C++ の歴史の中で重要な位置を占めています。C++98 標準にはさまざまな初期化形式があり、C++11 ではこの問題を根本的に改善する初期化リストが導入されています。
リストの初期化
C++98 標準ではさまざまな形式の初期化メソッドが存在するため、初期化問題は非常に複雑になっていますが、C++98 の厄介な問題を解決するために、C++11 では中括弧で初期化された変数が次のようになりました。完全に適用されています。この新しい形式の初期化はリスト初期化と呼ばれます。リストの初期化により、すべての型を {} の方法で均一に初期化できます。
int a[] = {
1,2,3 };
int aa[]{
1, 2,3 };
std::vector<int> aas{
1,2,3 };
std::unordered_map<std::string, int> az{
{
"1", 1}, {
"2", 2} };
絞り込まれたタイプ
型の絞り込みとは通常、データの変更や精度の低下を引き起こす可能性がある暗黙的な変換を指します。組み込み変数に適用される場合、リストの初期化には重要な機能があります。リストの初期化により型の絞り込みを防ぐことができます。初期化リストの初期化変数に情報損失のリスクがある場合、コンパイラはエラーを報告します。さらに、リスト初期化は、型の絞り込みを防ぐことができる C++11 の唯一の初期化メソッドであり、他の初期化と区別するリスト初期化の最も重要な機能でもあります。
// 浮点型到整型的转换
int aVal = 2.0; // C++98/0x 标准可以通过编译
int aVal2 = {
2.0}; // C++11 标准提示编译错误,类型收窄
// 整型到浮点型的转换
float c = 1e70; // C++98/0x 标准可以通过编译
float d = {
1e70}; // C++11 标准提示编译错误,类型收窄
型の絞り込みにつながる可能性のある一般的なシナリオは次のとおりです。
- 浮動小数点を整数に変換する
- 高精度浮動小数点数を低精度浮動小数点数に変換する
- 整数 (厳密に型指定されていない列挙型) から float への変換
- 整数 (厳密に型指定されていない列挙) は、より短い長さの整数に変換されます。
初期化子リスト
initializer_list は C++11 STL によって導入されています。任意の長さの初期化リストを解決できますが、パラメーターの型が T と同じであるか、暗黙的に T に変換できる必要があります。initializer_list の定義:
template <class T> class initializer_list
関数呼び出しを行うときは、中括弧を使用してすべてのパラメータを囲む必要があります。例えば:
void errorMsg(std::initializer_list<std::string> str) //可变参数,所有参数类型一致
{
for (auto beg = str.begin(); beg != str.end(); ++beg)
std::cout << *beg << " ";
std::cout << std::endl;
}
//调用
errorMsg({
"hello","error",error}); // error为string类型
errorMsg({
"hello2",well}); // well为string类型
initializer_list オブジェクトの要素は常に定数値であり、initializer_list オブジェクトの要素の値を変更できないことに注意してください。
C++17
初期化をさらに改善し、コピーの最適化の強制省略とリスト初期化タイプの導出という 2 つの機能を導入しました。リスト初期化の型推論は自動型推論に関するもので、コピーの強制省略の最適化はオブジェクトのコピー初期化、集合体初期化の拡張機能 (基本クラスを許可する) に関するものです。
リスト初期化型の推論
C++11 のリスト初期化ルールは、auto と組み合わせて使用するとプログラマの期待を満たせず、間違いを犯すことがよくあるため、C++17 ではリスト初期化ルールが強化されています。
C++17 のリスト初期化の機能強化は、次の 3 つのルールに要約できます。
- auto var {element}; var を element と同じ型として推測します
- auto var {element1, element2, …}; この形式は不正であるため、コンパイル エラーが発生します
- auto var = {element1, element2, …}; var は std::initializer_list として推定されます (T は要素の型)
auto var = {element1, element2, …};
この種の型推論では、すべての要素の型が同じであるか、同じ型に変換できる必要があります。
auto v {
1}; // 正确:v 推导为 int
auto w {
1, 2}; // 错误: 初始化列表只能为单元素
/* C++17 增加的新规则 */
auto x = {
1}; // 正确:x 推导为 std::initializer_list<int>
auto y = {
1, 2}; // 正确:y 推导为 std::initializer_list<int>
auto z = {
1, 2, 3.0}; // 错误: 初始化列表中的元素类型必须相同
コピー最適化の強制省略
C++ 標準は、パフォーマンスを向上させるために、一部の一時変数やコピー操作を削減しようとしています。これは、C++ 標準が解決しようとしている問題です。C++17 より前の一部のコンパイラは、このコピー操作の無視をすでにサポートしていますが、C++ 標準は必須ではありません。幸いなことに、C++17 では省略されたコピーが標準に組み込まれたため、C++17 以降では無視されたコピーが必須になります。
C++17 では、コピーの最適化の省略は、RVO (戻り値の最適化) と NRVO (名前付き戻り値の最適化)、例外のキャプチャ、およびオブジェクトの一時的な構築の 4 つのシナリオで現れると規定しています。
NRVO (名前付き戻り値の最適化)
関数が名前付き戻り値の最適化を返す場合、コピーの最適化の強制省略が有効になります。
class Thing
{
public:
Thing();
virtual ~Thing();
Thing(const Thing&);
};
Thing f()
{
Thing t;
return t;
}
Thing t2 = f();
上記のコードによって生成されたアセンブリ コード (https://godbolt.org/ はコード変換の実現に役立ちます)
f():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Thing::Thing() [complete object constructor]
nop
mov rax, QWORD PTR [rbp-8]
leave
ret
t2:
.zero 1
__static_initialization_and_destruction_0():
push rbp
mov rbp, rsp
mov eax, OFFSET FLAT:t2
mov rdi, rax
call f()
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:t2
mov edi, OFFSET FLAT:_ZN5ThingD1Ev
call __cxa_atexit
nop
pop rbp
ret
_GLOBAL__sub_I_f():
push rbp
mov rbp, rsp
call __static_initialization_and_destruction_0()
pop rbp
ret
C++17 標準では、 f() はオブジェクト構築を 1 回だけ呼び出し、生成されたオブジェクトを move によって t2 に転送することがわかります。C++17 より前の関数によって返される一時変数の生成と構築は無視されます。
RVO (戻り値の最適化)
C++17 では、関数が RVO (戻り値の最適化) を返す場合、コピーの最適化も強制的に省略されます。
class Thing
{
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing f()
{
return Thing();
}
Thing t2 = f();
上記のコードによって生成されたアセンブリ コード (https://godbolt.org/ はコード変換の実現に役立ちます)
f():
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Thing::Thing() [complete object constructor]
mov rax, QWORD PTR [rbp-8]
leave
ret
t2:
.zero 1
__static_initialization_and_destruction_0():
push rbp
mov rbp, rsp
mov eax, OFFSET FLAT:t2
mov rdi, rax
call f()
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:t2
mov edi, OFFSET FLAT:_ZN5ThingD1Ev
call __cxa_atexit
nop
pop rbp
ret
_GLOBAL__sub_I_f():
push rbp
mov rbp, rsp
call __static_initialization_and_destruction_0()
pop rbp
ret
C++17 標準では、 f() はオブジェクト構築を 1 回だけ呼び出し、生成されたオブジェクトを move によって t2 に転送することがわかります。C++17 より前の関数によって返される一時変数の生成と構築は無視されます。実装形態はNRVOと同様です。
一時的なものから構築
オブジェクトが一時変数から構築されている場合、この時点でコピーの最適化も強制的に省略されます。参考例:
class Thing
{
public:
Thing();
~Thing();
Thing(const Thing&);
};
Thing t2 = Thing();
Thing t3 = Thing(Thing());
生成されたアセンブリ コード (https://godbolt.org/ はコード変換の実現に役立ちます)
t2:
.zero 1
t3:
.zero 1
__static_initialization_and_destruction_0():
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:t2
call Thing::Thing() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:t2
mov edi, OFFSET FLAT:_ZN5ThingD1Ev
call __cxa_atexit
mov edi, OFFSET FLAT:t3
call Thing::Thing() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:t3
mov edi, OFFSET FLAT:_ZN5ThingD1Ev
call __cxa_atexit
nop
pop rbp
ret
_GLOBAL__sub_I_t2:
push rbp
mov rbp, rsp
call __static_initialization_and_destruction_0()
pop rbp
ret
上記のアセンブリ コードから、t2 と t3 は両方とも同じであるという結論を導き出すことができます。まずオブジェクトの構築を完了し、次に移動によってオブジェクトをそれらに転送します。複数オブジェクトのコピー構造は途中で無視されます。
値によってキャッチされた例外オブジェクト
例外スローが値によってキャッチされると、強制省略コピーの最適化が発生します。参考例:
struct Thing
{
Thing();
Thing(const Thing&);
};
void foo()
{
Thing c;
throw c;
}
int main()
{
try
{
foo();
}
catch(Thing c)
{
}
}
生成されたアセンブリ コード (https://godbolt.org/ はコード変換の実現に役立ちます)
foo():
push rbp
mov rbp, rsp
push r12
push rbx
sub rsp, 16
lea rax, [rbp-17]
mov rdi, rax
call Thing::Thing() [complete object constructor]
mov edi, 1
call __cxa_allocate_exception
mov rbx, rax
lea rax, [rbp-17]
mov rsi, rax
mov rdi, rbx
call Thing::Thing(Thing const&) [complete object constructor]
mov edx, 0
mov esi, OFFSET FLAT:typeinfo for Thing
mov rdi, rbx
call __cxa_throw
mov r12, rax
mov rdi, rbx
call __cxa_free_exception
mov rax, r12
mov rdi, rax
call _Unwind_Resume
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
call foo()
.L8:
mov eax, 0
jmp .L10
mov rbx, rax
mov rax, rdx
cmp rax, 1
je .L7
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L7:
mov rax, rbx
mov rdi, rax
call __cxa_get_exception_ptr
mov rdx, rax
lea rax, [rbp-17]
mov rsi, rdx
mov rdi, rax
call Thing::Thing(Thing const&) [complete object constructor]
mov rax, rbx
mov rdi, rax
call __cxa_begin_catch
call __cxa_end_catch
jmp .L8
.L10:
mov rbx, QWORD PTR [rbp-8]
leave
ret
typeinfo for Thing:
.quad vtable for __cxxabiv1::__class_type_info+16
.quad typeinfo name for Thing
typeinfo name for Thing:
.string "5Thing"
アセンブリコードを分析すると、catch(Thing c)のcオブジェクトはvoid foo()のcオブジェクトから直接コピーされており、コピー構築は1回だけなので、ここではコピー最適化の強制省略が有効であることがわかります。
継承を許可する集合体の初期化
C++17 以降、集約には基本クラスを持つこともでき、他のクラス/構造体から派生した構造体を初期化できます。
struct NewPerson: AggrPerson
{
bool isMan;
};
NewPerson person{
{
"liuguang", 20}, true};
集約の初期化では、ネストされた括弧を使用した基本クラスのメンバー変数への初期値の受け渡しがサポートされるようになりました。括弧は省略することもできます。
NewPerson person{
"liuguang", 20, true};
C++17 以降、集計は次のように定義されます。
- 配列にすることもできます
- または以下を必要とするカスタム型 (クラス、構造体、または共用体)
– ユーザー定義または明示的なコンストラクター
なし – using 宣言を介して継承されたコンストラクターなし
– プライベートまたは保護された非静的メンバー変数なし
– 仮想関数なし
– 仮想、プライベート、または保護された基本クラス
集約を使用できるようにするには、初期化中にプライベートまたは保護された基本クラスのメンバーまたはコンストラクターが含まれていないことも必要です。
struct A // C++17聚合体
{
A() = delete;
};
struct B // C++17聚合体
{
B() = default;
int i = 0;
};
struct C // C++17聚合体
{
C(C&&) = default;
int a, b;
};
A a{
}; // C++17合法
B b = {
1}; // C++17合法
auto* c = new C{
2, 3}; // C++17合法
C++17 では、型が集合体であるかどうかをテストするための新しい型抽出 is_aggregate<> を導入しています。
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
D<float> s{
{
"hello"}, {
4.5,6.7}, "world"}; // C++17开始正确
std::cout << std::is_aggregate<decltype(s)>::value; // 输出: 1
C++20
C++20 では、集合体の初期化がさらに最適化され、改善されています。具体的な最適化には、次の 2 つの側面が含まれます。1 つ目は、集合体がユーザー宣言のコンストラクターを持つことを禁止すること、2 つ目は、集合体が C 言語の指定された初期化と互換性があることです。
集約コンストラクターを禁止する
C++20 [P1008R1]提案では、コンストラクターが宣言されている限り、この class&struct は集合体ではないことが要求されます。例えば:
struct A // C++17及以前是聚合体,C++20 非聚合体
{
A() = delete;
};
struct B // C++17及以前是聚合体,C++20 非聚合体
{
B() = default;
int i = 0;
};
struct C // C++17及以前是聚合体,C++20 非聚合体
{
C(C&&) = default;
int a, b;
};
A a{
}; // C++20不合法,C++17及以前合法
B b = {
1}; // C++20不合法,C++17及以前合法
auto* c = new C{
2, 3}; // C++20不合法,C++17及以前合法
指定された初期化
C++20 でサポートされている次の 2 つの指定された初期化メソッドを使用できます。
T 对象 = {
.指定符1 = 实参1 , .指定符2 {
实参2 } ... };
T 对象 {
.指定符1 = 实参1 , .指定符2 {
实参2 } ... };
- 各指定子は T の直接の非静的データ メンバーを指定する必要があり、式で使用されるすべての指定子は T のデータ メンバーと同じ順序で現れる必要があります。例えば:
struct A
{
int x;
int y;
int z;
};
A a{
.y = 2, .x = 1}; // Error:指定符的顺序不匹配声明顺序
A b{
.x = 1, .z = 2}; // OK:b.y 被初始化为 0
- 指定された初期化子によって指定された直接の非静的データ メンバーはそれぞれ、対応する中括弧または等号指定子に続く指定子から初期化されます。絞り込み変換は禁止されています。
- 指定された初期化子を使用して、共用体を最初のメンバー以外の状態に初期化できます。共用体には初期化子を 1 つだけ指定できます。
union u
{
int a;
const char* b;
};
u f = {
.b = "asdf"}; // OK:联合体的活跃成员是 b
u g = {
.a = 1, .b = "asdf"}; // Error:只可提供一个初始化器
- 指定された初期化子が提供されていない非共用体集合体の要素は、初期化子節の数がメンバーの数より少ない場合に、上記の規則に従って初期化されます (デフォルトのメンバー初期化子が提供されている場合は使用され、そうでない場合は空のメンバー初期化子が使用されます)。リストの初期化)
struct A
{
string str;
int n = 42;
int m = -1;
};
A{
.m = 21} // 以 {} 初始化 str,这样会调用默认构造函数
// 然后以 = 42 初始化 n
// 然后以 = 21 初始化 m
- 指定初期化子句によって初期化された集合体に匿名共用体メンバーがある場合、対応する指定初期化子は匿名共用体のメンバーの 1 つを指定する必要があります。
struct C
{
union
{
int a;
const char* p;
};
int x;
} c = {
.a = 1, .x = 3}; // 以 1 初始化 c.a 并且以 3 初始化 c.x
- アウトオブオーダーの指定初期化、ネストされた指定初期化、指定初期化子と通常の初期化子の混合、および配列の指定初期化は、C プログラミング言語ではサポートされていますが、C++ ではサポートされていません。
struct A {
int x, y; };
struct B {
struct A a; };
struct A a = {
.y = 1, .x = 2}; // C 中合法,C++ 中非法(乱序)
int arr[3] = {
[1] = 5}; // C 中合法,C++ 中非法(数组)
struct B b = {
.a.x = 0}; // C 中合法,C++ 中非法(嵌套)
struct A a = {
.x = 1, 2}; // C 中合法,C++ 中非法(混合)
要約する
変数の初期化は C++ 標準の重要な部分です。C++ の初期化は、コピーの有無によって直接初期化とコピー初期化に分けられ、その他の初期化はこれら 2 つのカテゴリに分類できます。この分類基準に基づいて、この記事では、C++98 標準の直接初期化、コピー初期化、集合体初期化、および括弧初期化、C++11 標準のリスト初期化とInitializer_list、C 言語のコピー最適化の必須省略について紹介します。 ++17 標準、リスト初期化型の推定、集合体の初期化拡張 (基本クラスを許可)、C++20 標準の集合体初期化の拡張、指定された初期化子の初期化。