Upload
-
View
66
Download
3
Embed Size (px)
Citation preview
Effective Modern C++読書会#2
Item7,8
© 2015 sprout Inc. All Rights Reserved.1
株式会社スプラウト高橋 明Twitter : @Talos208
Item7
Distinguish between () and {}
when creating objects.
© 2015 sprout Inc. All Rights Reserved.2
C++11の初期化方法
© 2015 sprout Inc. All Rights Reserved.3
int x( 0 ); // (1)括弧で囲った初期化子
int y = 0; // (2) "="に続く初期化子
int z{ 0 }; // (3) 中括弧で囲った初期化子
int z = { 0 }; // (4) "="に続き、中括弧で囲った初期化子
(4)は(3)と同じに扱われる。
初期化時の“=”
• 初期化時の“=”は代入ではない
• C++初心者が間違えやすい点
© 2015 sprout Inc. All Rights Reserved.4
Widget w1; // デフォルトコンストラクタの呼び出し
Widget w2 = w1; // 代入ではなく、コピーコンストラクタの呼び出し
w1 = w2; // 代入。operator()が呼び出される
「統一的な初期化構文」
// 複数要素の代入
std::vector<int> v{1,3,5}; // 1,3,5の3要素を格納したvector
// メンバ変数の初期化
class Widget {
...
private:
int x{ 0 }; // x を初期値0で初期化
int y = 0; // y を初期値0で初期化
int z(0); // エラー!
}
// コピーできないオブジェクト(例:std::atmic)には“=”が使えない
std::atomic<int> ai0{ 0 }; // OK
std::atomic<int> ai1(0); // OK
std::atomic<int> ai2 = 0; // エラー!
© 2015 sprout Inc. All Rights Reserved.5
“{}”内でのnarrowingの禁止
double x,y,z;
...
int sum1{ x + y + z }; // エラー。doubleはintで表せない場合がある
int sum2( x + y + z ); // OK。暗黙の型変換でintになる
int sum3 = x + y + z; // 同上
© 2015 sprout Inc. All Rights Reserved.6
後者2つの挙動を変えると、あまりに多くのレガシーコードを破壊してしまう。
「Most Vexing Parse (最も厄介な解析)」
Widget w1(10); // 10を引数としたWidget型変数の初期化
// 同様に引数なしの初期化を意図して次のように書くと、
// コンパイラに誤って解釈される
Widget w2(); // Widgetを返す関数w2の宣言に解釈される
// 以下のように記述すれば意図通り
Widget w3{}; // 引数なしでのWidget型変数の初期化
Widget w4; // これで良くない?
© 2015 sprout Inc. All Rights Reserved.7
std::initializer_listとコンストラクタ
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
...
};
Widget w1(10, true); // 最初のコンストラクタが呼ばれる
Widget w2{10, true}; // 同上
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 同上
© 2015 sprout Inc. All Rights Reserved.8
std::initializer_listとコンストラクタ(2)
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il); // これを追加
...
};
Widget w1(10, true); // 変化なし
Widget w2{10, true}; // 3番目のコンストラクタが呼ばれる!
// 10とtrueの両方がlong doubleにキャスト
Widget w3(10, 5.0); // 変化なし
Widget w4{10, 5.0}; // 3番目のコンストラクタが呼ばれる!
// 10と5.0の両方がlong doubleにキャスト
© 2015 sprout Inc. All Rights Reserved.9
コピー/ムーブのコンストラクタ
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);
operator float() const; // floatへの暗黙の型変換
...
};
Widget w4;
Widget w5(w4); // w4からのコピーコンストラクタが呼ばれる
Widget w6{w4}; // 3番目のコンストラクタが呼ばれる!
// w4がfloatにキャストされ、さらにlong doubleに変換
Widget w7(std::move(w4)); // w4からのムーブコンストラクタが呼ばれる
Widget w8{std::move(w4)}; // 3番目のコンストラクタが呼ばれる!
© 2015 sprout Inc. All Rights Reserved.10
std::initializer_listの優先度
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<bool> il);
// bool型のinitializer_listのコンストラクタ
// 暗黙の型変換を行うメソッドはない
...
};
Widget w4{10, 5.0}; // エラー!
© 2015 sprout Inc. All Rights Reserved.11
コンパイラは(他のコンストラクタに優先して)std::initializer_list<bool>を呼ぼうとしてint(10)とdouble(5.0)をboolに変換する(intまたはdoubleからboolへの暗黙の変換はできる)この変換はnarrowingなので“{}”の中ではできず、エラーとなる。
std::initializer_list<T>のTへの変換がそもそもできない場合
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<std::string> il);
// boolからstd::stringに変更
...
};
Widget w1(10, true); // 最初のコンストラクタが呼ばれる
Widget w2{10, true}; // 最初のコンストラクタが呼ばれるようになる
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 2番目のコンストラクタが呼ばれるようになる
© 2015 sprout Inc. All Rights Reserved.12
初期化子のない“{}”
class Widget {
public:
Widget();
Widget(std::initializer_list<int> il); // boolからstd::stringに変更
...
};
Widget w1; // デフォルトコンストラクタが呼ばれる
Widget w2{}; // 同様にデフォルトコンストラクタが呼ばれる
Widget w3(); // 最も厄介な解析! 関数宣言になる
// 0要素のstd::initializer_list<>として呼びたいなら“()”か“{}”で囲む
Widget w4({}); // std::initializer_list<int>のコンストラクタが呼ばれる
Widget w5{{}}; // 同上
© 2015 sprout Inc. All Rights Reserved.13
0要素のstd::initializer_listでなく、引数がないことを表す
std::vector<T>とstd::initializer_list
std::vector<int> v1(10,20);
// サイズと初期値を指定するコンストラクタが呼ばれる。
// 各要素が20で初期化された、10要素のvectorができる
std::vector<int> v2{10,20};
// std::initializer_list<int>のコンストラクタが呼ばれる。
// 10と20で初期化された、2要素のvectorができる
© 2015 sprout Inc. All Rights Reserved.14
• それまでstd::initializer_list<>を引数に持つコンストラクタがないクラスに、それを引数に持つコンストラクタを1つでも加えると、互換性を破壊するおそれがある。
• クラスの提供者は、クラスにstd::initializer_list<>を引数に持つコンストラクタを加えるとき、熟考が必要。
• クラスの使用者も、初期化時に"()”と“{}”のどちらを使うか慎重に検討する必要がある。
Pros and Cons
• “{}”の使用
• 適用可能なケースが多い
• 間違えてnarrowingしてしまう可能性が低い
• Most Vexing Parseの心配がない
• “()”の使用
• C++98の文法と互換
• std::initializer_listに関する問題がない
© 2015 sprout Inc. All Rights Reserved.15
テンプレート
// paramsを引数としてローカルのT型のオブジェクトを作成
template<typename T, // 作成するオブジェクトの型
typename... Ts> // 使用する引数の型
void doSomeWork(Ts&&... params){
T localObject(std::forward<Ts>(params...)); // “()”を使う場合
// T localObject{std::forward<Ts>(params...)}; // “{}”を使う場合
}
Std::vector<int> v;
doSomeWork<std::vector<int>>(10,20); // 10要素のvectorが正しいのか?
// 2要素のvectorが正しいのか?
© 2015 sprout Inc. All Rights Reserved.16
doSomeWorkの結果として何が正しいのか、doSomeWorkを書く側では判らない。呼び出し側に依存する。Std::make_uniqueやstd::make_shareでは、内部で“()”を使用して、それをドキュメントに記載している。
Things to Remember
• “{}”による初期化は、適用可能なケースが多く、narrowing変換を防げ、C++のMost Vexing Parseの心配がない
• コンストラクタオーバーロードの解決において、“{}”による初期化は、他にもっと適したコンストラクタがあっても、可能であればstd::initializer_listに割り当てられてしまう
• “()”と“{}”とで大きな違いを産んでしまう例の一つが、std::vector<数値型>のコンストラクタを2つの引数で呼ぶ場合である
• テンプレート内部で“()”と“{}”のどちらを使うかは難しい問題だ
© 2015 sprout Inc. All Rights Reserved.17
Item 8
Prefer nullptr to 0 and NULL.
© 2015 sprout Inc. All Rights Reserved.18
0とNULLとnullptr
• C++ではポインタが使えるところに0があった場合、ヌルポインタにフォールバックするが、リテラルの0はintであってポインタではない。NULLもほぼ同様だが、int以外の整数型としても使えるところが違う。
• リテラルとしてのNULLに決まった型はない。C++98では、ポインタと整数型のオーバーロードに0やNULLを渡した場合、ポインタのオーバーロードは呼ばれない。
• NULLが、例えば0Lと定義されていた場合、long→int、long→bool、0L→void*のいずれの変換も同様に正しいので、どれかを選びようがない。
• nullptrの型はstd::nullptr_tで、すべてのポインタ型に変換可能
© 2015 sprout Inc. All Rights Reserved.19
void f(int);
void f(bool);
void f(void*);
f(0); // f(void*)でなくf(int)が呼ばれる
f(NULL); // ビルドエラーになるかもしれないが、たいていは
// f(int)が呼ばれる。f(void*)は呼ばれない
f(nullptr); // f(void*)が呼ばれる
Nullptrより0がよい場合
auto result = findRecord( /* ここに引数 */ );
if (result == 0) {
...
}
© 2015 sprout Inc. All Rights Reserved.20
resultの型がわからなくても、0ならば整数型でもポインタでもどちらでもOK
テンプレートとnullptr
• ミューテックスでガードしなければならない関数f1、f2、f3がある。単純に書くと
© 2015 sprout Inc. All Rights Reserved.21
int f1(std::shared_ptr<Widget> spw);
int f2(std::unique_ptr<Widget> spw);
int f3(*Widget spw);
std::mutex f1m, f2m, f3m;
using MuxGuard = std::lock_guard<std::mutex>;
{
MutexGuard g(f1m); // ロック
auto result = f1(0);
// スコープを抜けるところでロック解放
}
...
{
MutexGuard g(f2m);
auto result = f1(NULL);
}
...
{
MutexGuard g(f3m);
auto result = f3(nullptr);
}
テンプレートとnullptr
• 毎回ロックするのはあんまりなので、テンプレートを使って以下のようにしてみると
© 2015 sprout Inc. All Rights Reserved.22
template<typename FuncType,
typename MuxType,
typeName PtrType>
auto lockAndCall(FuncType func,
MutexType& mutex,
PtrType ptr) -> decltype(func(ptr)) // Item3参照
{
MuxGuard g(mutex);
return func(ptr);
}
auto result1 = lockAndCall(f1, f1m, 0); // エラー!
auto result2 = lockAndCall(f2, f2m, NULL); // エラー!
auto result3 = lockAndCall(f3, f3m, nullptr); // 成功
テンプレートとnullptr
• 0やNULLリテラルの代入なら、その場で(ヌルポインタに)変換されて渡される。
• テンプレートに与えた場合はテンプレートとしての型解決が先に動いてしまう。
• std::shared_ptr<Widget>やstd::unique_ptr<Widget>にintを渡そうとしてエラー
• nullptrはstd::nullptr_t型なので問題無し。
© 2015 sprout Inc. All Rights Reserved.23
Things to Remember
• 0やNULLでなくnullptrを使おう
• 整数型とポインタのオーバーロードは止めよう
© 2015 sprout Inc. All Rights Reserved.24