サバです。
3章にはいります!
今回も、表とその下の解説は無駄に長いので、適当に流し読みしてください!
<以下長い注意>
ここの内容は、サバが適当な自己解釈を重ねた結果、攪拌され白濁してしまった知識の泉から、沈殿物を素手で持ち上げようとした結果です。保証はしかねます。ご了承ください。
内容はEffectiveModernC++をわざわざ書き写したような何かです。
題名にEffectiveModernC++が入っていないのは更新が途中で止まる自信があるからです。
また、もしよろしければ、間違い、気になったところ、分かりづらいところなどを指摘してもらえると、サバがピチピチ跳ねて喜びます。喜びすぎてプログラミング言語C++第四版をあなたに投げつけるかもしれません。
(ぜひ教えてください。よろしくお願いします。 < ( _ _ ) > 何でもするとは言いませんから…)
項目7 オブジェクト作成時の()と{}の違い
結論:{}を使うならstd::initializer_listコンストラクタに気をつける
C++11はオブジェクト作成時の初期化に()と{}を使い分けられます。ただし、組み込み型(プリミティブ型)変数の初期化には = も使えるため、初期化と代入がごっちゃに理解されることが多いようです。自分もそうでした。
・初期化方法まとめ
重要なのは組み込み型の = 初期化が特殊であることです。
ここではユーザ定義型(構造体・クラス)の例として Widget という型名を用います。
組み込み型 (クラス・構造体以外) |
int Integer1 = 0; int Integer2( 0 ); int Integer3{ 0 }; |
すべて同じ意味 0 で int 型変数を作成・初期化 = でも初期化をすることができる 初期化であって代入ではない |
ユーザ定義型 (クラス・構造体) |
Widget widget1( 0 ); Widget widget2{ 0 }; |
すべて同じ意味 Widgetのコンストラクタに 0 を渡して、Widgetオブジェクトを作成・初期化 |
- ここにでは省略しましたが、{} の代わりに = {} も使用できます。全く同じ意味です。
- ちなみにユーザ定義型で、下コード widget5 のように初期化をしない場合、デフォルトコンストラクタによって暗黙的に初期化されます。個人的には初期化を忘れたバグの根源なのか、デフォルト初期化したいのかが分からないので、widget6 の明示的初期化が良いと思います。
Widget widget5;
Widget widget6{};どちらもデフォルトコンストラクタで初期化 - コンパイラによっては1変数を受け取るユーザ定義型のコンストラクタなら = でも初期化できるようです。
・初期化の統一記法
C++は変態なので変数を初期化する場所・場合がたくさんあります。
そんな中でも {} だけは統一的にすべての初期化を行うことができます。{}すんごい!!
組み込み型 | int integer1 = 0; int integer2(); int integer3{}; |
= () {} 全てで初期化できます。 全てデフォルト値0での初期化です。 |
ユーザ定義型 |
Widget widget1(); Widget widget2{}; // NG : 二度手間 Widget widget3 = Widget{}; |
= では期待通りの初期化はできません。一時オブジェクトを作成し、ムーブコンストラクタでムーブ初期化するという二度手間になります。 (最適化されなければ) |
宣言初期化子 |
class Widget { int member1 = 0; int member2{}; // メソッド? int member3(); }; |
() では初期化できません。 コンパイラにとってはメソッド (メンバ関数)宣言にしか見えません。 |
コンストラクタ初期化子 | class Widget { int member1; int member2; public: |
= では初期化できません。 メンバを初期化するためのもので、代入するためのものではありません。(適当に理由を付けました) |
コピーできないオブジェクト | std::atomic<int> ai{ 0 };
// Error : 関数は削除されてる |
コピーやムーブを出来ないように設計されたオブジェクトは = 等を使えません。
左の例ではムーブコンストラクタが消されているため、エラーとなります。 |
template との組み合わせ |
template<typename T> decltype(auto) func() { // どっち? return T::Whats(); } |
こんな状況で () 初期化を使うのはやめましょう。Whats という static 関数呼び出しにしか見えません。 このコードで Whats オブジェクトの初期化をしていた時は、バグの発見に大分骨を折りそうです。 |
- 上の例を見ると {} だけいつでも初期化に使えることが分かります。
・ {} 初期化の型チェック機能
{} 初期化は統一記法以外にも型チェックという利点があります。
縮小変換(精度が落ちる変換)が必要な初期化が行われた場合、コンパイラがエラーを出してくれます。(コンパイラによっては警告)
int pi { 3.14 }; // Error double x{}, y{} z{}; int sum { x + y + z }; // Error |
double 型から int 型への暗黙的な縮小変換 (精度が落ちる・値が壊れるような型変換) |
bool flag { 2 }; // Error | int 型から bool 型への暗黙的な縮小変換 |
- エラーを避けるためには static_cast を使いましょう
int pi{ static_cast<int>( 3.14 ) };
・{} 初期化の落とし穴
{} は凄い!ですが、致命的な問題があります。
それは std::initializer_list との相性問題です。
std::initializer_list を取るコンストラクタを持つオブジェクトを {} 初期化すると、縮小変換をしてでも、力ずくでこの std::initializer_list コンストラクタを使おうとします。
class Widget { // コンストラクタ1 Widget( int value ) {} // コンストラクタ2 Widget( std::initializer_list<bool> list ) {} }; int main() return 0; |
main 関数内で、Widget の コンストラクタ1 を呼び出しているように見えますが、 コンストラクタ2 が呼び出されます。 コンストラクタ1 を呼び出すには () を使った初期化をする必要があります。 |
- コンパイラには {} が std::initializer_list にしか見えないのでしょうか…
- 力ずくで変換できない場合、つまり、暗黙の型変換方法がない場合は、こんなことになりません。 例えば、std::initializer_list<bool> ではなく、 std::initializer_list<std::string>であった場合は、int 型から std::string 型への暗黙の型変換方法がないため、コンストラクタ1 が呼び出されます。
残念ながらこの相性問題の代表例は STL です。
std::vector も std::initializer_list をとるコンストラクタを持つため、こんな頭痛が痛い問題が起きます。
// { 10, 1 } std::vector<int> vec1{ 10, 1 }; |
要素数2で、それぞれの値が 10, 1 となる std::vector を作成 |
// { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } std::vector<int> vec2( 10, 1 ); |
要素数10で、すべての値が 1 となる std::vector を作成 |
// { 100 } std::vector<int> vec3{ 100 }; |
要素が 100 のみで要素数1となる std::vector を作成 |
// { 0, 0, 0, … ( x 100) } std::vector<int> vec4( 100 ); |
要素数100で、すべての要素をデフォルト値( 0 )で初期化した std::vector を作成 |
- std::vector は {} だけじゃすべての初期化ができない… {} は初期化の統一記法じゃなかった!!!
初期化するオブジェクトに std::initializer_list をとるものがないか事前に確認しましょう。std::initializer_list をとるコンストラクタを定義するときは本当に必要なのか考えましょう。
Effective Modern C++ の中では、(), {} どちらの初期化も長所・短所を持つため、いずれかの記法を優先して使うように決め、必要に応じてもう一方を使おうとまとめられています。
< 前の項目へ 次の項目へ > // そのうち…