2014年8月18日月曜日

【読書ノート】Effective Modern C++ - Item 7

O'Reilly Early Releaseで購入したEffective Modern C++のドラフト版の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 7: オブジェクトを生成する時、()と{}を区別しよう

以下の文は全てint型変数を0で初期化する

int x(0);
int y = 0;
int z{0};
int z = {0};

初期化と代入の区別

Widget w1; // デフォルトコンストラクタが呼ばれる
Widget w2 = w1; // コピーコンストラクタが呼ばれる
w1 = w2; // 代入(operator=が呼ばれる)

C++11で追加された{} (braced initialization) はuniform initializationの実装。
{}を使うと、C++11以前はできなかったコンテナの初期化が可能。

std::vector<int> v{1, 2, 3};

初期化方法に違いが生じるケース

C++11では、非staticデータメンバの初期化が可能になったが、()ではできない

class Widget {
private:
    int x{0}; // OK
    int y = 0; // OK
    int z(0); // エラー!
}

std::atomicsなどのコピー不可能なオブジェクトは=で初期化できない

std::atomic<int> ai1{0}; // OK
std::atomic<int> ai2(0); // OK
std::atomic<int> ai3 = 0; // エラー!
{}は組み込み型のnarrowing conversionを禁止する
double x, y, z;
...
int sum1{x + y + z}; // エラー! doubleからintへの変換に非対応
int sum2(x + y + z); // OK double型の値がint型に丸められる
int sum3 = x + y + z; // OK 同上
{}はmost vexing parseを起こさない
Widget w1(10); // 10を引数に取るWidgetのコンストラクタが呼ばれる
Widget w2(); // most vexing parse! Widget型を返す関数の宣言と解釈される
void f(const Widget& w = Widget()); // Widgetのデフォルトコンストラクタが呼ばれる

Widget w1{10}; //コンストラクタが呼ばれる
Widget w2{}; // デフォルトコンストラクタが呼ばれる
void f(const Widget& w = Widget{}); //デフォルトコンストラクタが呼ばれる

{}の直感に反する動作

autoは、{}で初期化されるオブジェクトをstd::initializer_list型と型推定する

auto v1 = -1; // v1はint型
auto v2(-1); // v2はint型
auto v3{-1}; // v3はstd::initializer_list<int>型
auto v4 = {-1}; // v4はstd::initializer_list<int>型

std::initializer_listの無いコンストラクタは通常の動作

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // 同上
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 同上

しかしstd::initializer_listがある場合、{}は常にそちらを優先する

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // std::init_listのコンストラクタが呼ばれる
                     // 10とtrueはlong doubleに変換される
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // std::init_listのコンストラクタが呼ばれる
                     // 10と5.0はlong doubleに変換される

narrowing conversionが生じてコンパイルエラーになってもstd::initializer_listを優先する

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<bool> il);
    ...
};

Widget w{10, 5.0}; // narrowing conversionによるコンパイルエラー!

しかし変換が存在しない場合は他の候補を読みに行く

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<std::string> il);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // 1番目のコンストラクタが呼ばれる
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 2番目のコンストラクタが呼ばれる

空の{}は空のstd::initializer_listではなく引数が無いことを表す

class Widget {
public:
    Widget();
    Widget(std::initializer_list<int> il);
    ...
};

Widget w1; // デフォルトコンストラクタが呼ばれる
Widget w2{}; // デフォルトコンストラクタが呼ばれる
Widget w3(); // most vexing parse! 関数が宣言される
Widget w4({}); // std::init_listのコンストラクタが呼ばれる(リストは空)
Widget w5{{}}; // 同上

コピー・ムーブコンストラクタは通常通り呼ばれる
(Widget型であれば、たとえint型への変換が定義されていても、コピーコンストラクタ・ムーブコンストラクタが呼ばれる)

class Widget {
public:
    Widget(const Widget& rhs);
    Widget(Widget&& rhs);
    Widget(std::initializer_list<int> il);
    operator int() const;
    ...
};

auto w6{w5}; // コピーコンストラクタが呼ばれる
auto w7{std::move(w5)}; // ムーブコンストラクタが呼ばれる

これらのような難解なルールは、std::vectorという身近な例にも影響を与える。

std::vector<int> v1(10, 20); // 20を値とする要素を10個持つvector
std::vector<int> v2{10, 20}; // 10と20の2つの要素を持つvector

クラスデザインのガイドライン

ユーザーが()を使っても{}を使っても同じコンストラクタが呼ばれるデザインが良い。(その点においてstd::vectorは悪い例。)

std::initializer_listを引数に取るコンストラクターが無い状態からそれを新たに加える場合、既存のコードがその新しいコンストラクターを呼び出すように変化する危険性が生じる。
そのような改変は、十分に熟考してから行うべきである。

()と{}の利用ガイドライン

選択肢1: 原則{}を使う

{}は最も広範囲に適用でき、narrowing conversionを防ぎ、C++のmost vexing parseを起こさないという理由で{}を使用する。 ただし、先述のように{}を使用できないいくつかの場合において()を使用する。

選択肢2: 原則()を使う

()はC++98の文法と互換性があり、autoによるstd::initializer_list型推定の問題を回避し、std::initializer_listコンストラクタが意図せず呼び出されることが無いということを重んじて、()を使用する。 ただし、vectorの初期化などのように{}が必要な場面では{}を使用する。

片方の選択肢がもう片方より優れているということはない。
どちらかを選び、一貫した姿勢でコードを書くと良いだろう。

template関数やクラスを設計する場合はさらに事情が複雑になる。

template<typename T, typename... Args>
void doSomeWork(const T& obj, Args&&.. args)
{
    T localObject(std::forward<Args>(args)...);
    T localObject{std::forward<Args>(args)...};
}

これにたとえばユーザーがvectorを入れると、先述のように異なる結果になってしまう。

std::vector<int> v;
...
doSomeWork(v, 10, 20);

std::make_uniqueとstd::make_sharedはこの問題に直面し、()を採用してその旨をインターフェースに記述することで解決した。 他には、タグディスパッチによる解決方法もある。(Item 29)

Things to Remember

  • {}による初期化は最も広く適用可能な初期化であり、narrowing conversionを防ぎ、most vexing parseを起こさない
  • Item 2の通り、autoは{}で初期化されたオブジェクトをstd::initializer_list型と型推定する
  • {}による初期化は、std::initializer_listをパラメータに持つコンストラクタがある場合、たとえ他のコンストラクタの方がマッチしててもそちらが呼ばれる。
  • ()と{}のどちらで初期化するかで違いが生じる例はstd::vectorの2引数コンストラクタ
  • template内のオブジェクト生成において()と{}のどちらを選ぶかという決断は困難になりうる

2014年8月9日土曜日

【読書ノート】Effective Modern C++ - Item 6

O'Reilly Early Releaseで購入したEffective Modern C++のドラフト版の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 6はproxy classに対して使用するtyped initializerイディオムが紹介されていました。 どちらも初見の用語でした。 このイディオムを使用するのは一見無駄に見えましたが、確かにコードに意図を込めるには必要なのかなと思いました。

Item 6: typed initializerイディオムに気をつけよう

autoが意図しない型推定をしてしまうケース

std::vector<bool> features(const Widget& w);

// OK
auto highPriority = features(w)[5];
processWidget(w, highPriority);

// 未定義動作に!
auto highPriority = features(w)[5];
processWidget(w, highPriority);

autoにするとhighPriorityはboolではなくstd::vector<bool>::reference型になる。 bool&型にならない理由は、std::vector<bool>はビットで真偽値を格納するように特殊化されており、bool&のように振る舞う型を返すため。 features(w)はstd::vector<bool>の一時オブジェクトを返すことに注意。 この一時オブジェクトはhighPriorityの初期化の命令文が終わった時点で破棄され、std::vector<bool>::referenceは無効なポインタとなってしまう。

proxy class

std::vector<bool>::referenceは、他の型をエミュレートしたり増強したりする"proxy class"の例。 スマートポインタもproxy class。 proxy classの有用性はデザインパターンの"Proxy"パターンに由来する。 スマートポインタはstd::shared_ptrのように目に見えるproxy、std::vector<bool>::referenceは目に見えないproxy。

以下はExpression templatesもproxy classを使用した例。

Matrix sum = m1 + m2 + m3 + m4;

operator+がMatrixオブジェクトではなくSum<Matrix, Matrix>というproxy classを返すことで無駄な処理を省いて高速化する。 結果として右辺はSum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>型となり、それがMatrix型に暗黙の型変換がされる。

一般的に、目に見えないproxy classはautoと相性が悪い。 proxy classのオブジェクトはたいてい一時オブジェクトで、文が終わる時に寿命も尽きてしまうためだ。

目に見えないproxy classが使われているかどうかは、そのライブラリのドキュメントやヘッダーファイルを読めば分かる。

template <class Allocator>
class vector<bool, Allocator> {
    public:
    class reference{ ... };

    reference operator[](size_type n);
    ...
};

typed initializerイディオム

proxy classに対してはtyped initializerイディオムを使うと良い。

auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

このメリットは以下の例のように、戻り値と異なる型を指定しているのが意図的であることをはっきり示す点である。

double calcEpsilon();
float ep = calcEpsilon(); // 誤ってfloatを指定したのかと疑われる
auto ep = static_cast<float>(calcEpsilon()); //意図的にfloatにしたのだと分かる

// dは0.0と1.0の間の小数だとする
int index = d * c.size(); // doubleからintへの変換の意図が弱い
auto index = static_cast<int>(d * c.size()); // こちらの方が意図が伝わる

Things to Remember

  • 目に見えないproxy classはautoに「誤った」型推定をさせてしまう
  • typed initializerイディオムでautoに意図した型を推定させることができる

2014年8月2日土曜日

【読書ノート】Effective Modern C++ - Item 5

O'Reilly Early Releaseで購入したEffective Modern C++のドラフト版の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

第2章はautoについてです。
item 5では、変数宣言にautoを使うとどのようなメリットがあるのかが書かれていました。

Chapter 2 auto

autoのアイデア自体はC++の作者であるBjarne Stroustrupが持っていたが、当時としては冒険的すぎたので眠っていた。
それが30年近く経って、C++11に登場した。
おそらくC++11で最も使用される機能だが、時に直感に反する挙動をするので正しく使用するように注意する必要がある。

Item 5: 目地的な型宣言よりもautoを使おう

メリット1. 変数の初期化忘れを防げる

int x; // 変数が初期化されていない
auto x; // イニシャライザが無いので型推定できずコンパイルエラー
auto x = 0; // OK。xは正しく定義されている

メリット2. ローカル変数の宣言が簡潔に書けるように

// autoを使わない場合
template<typename IT>
void dwim(It b, It e)
{
    while (b != 0) {
        typename std::iterator_traits<It>::value_type
            currValue = *b;
        ...
    }
}

// autoを使った場合
template<typename IT>
void dwim(It b, It e)
{
    while (b != 0) {
        auto currValue = *b;
        ...
    }
}

メリット3. ラムダ式を格納する最も効率的な型として振る舞う

ラムダ式はコンパイラしか知らない型だが、autoを使えばそれを表現できる。

auto derefUPLess =
    [](const std::unique_ptr<Widget>& p1,
       const std::unique_ptr<Widget>& p2)
    { return *p1 < *p2; }

C++14では、パラメータもautoにすることが可能。

auto derefUPLess =
    [](auto& p1,
       auto& p2)
    { return *p1 < *p2; }

std::functionを使って下記のように記述することも可能だが、autoに比べて記述が長くなる上、std::functionには実行速度や使用メモリのオーバーヘッドがある。
また、ヒープを使用する場合もあるので、out-of-memory例外が発生する可能性もある。
autoはstd::bindの結果を格納するのにも有効である。(ただしitem 36にあるように、std::bindよりもラムダ式の方が好ましい。)

std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)>
    derefUPLess = [](const std::unique_ptr<Widget>& p1,
                     const std::unique_ptr<Widget>& p2)
                    { return *p1 < *p2; }

メリット4. 小さい型への代入を防げる

std::vector<int> v;
unsigned sz = v.size();
auto sz = v.size(); // std::vector<int>::size_typeとなる

32bit環境では問題ないが、64bit環境では(32bitである)unsigned型だと問題が起こる可能性がある。
64bit環境ではstd::vector<int>::size_typeは64bit整数であり、unsigned型に入れると32bit整数の最大値を超える整数はオーバーフローにより不正な値になってしまう。

メリット5. 明示的な型指定で起こりやすいミスを防げる

std::unordered_map<std::string, int> m;
...
for (const std::pair<std::string, int>& p : m)
{
    ... // pで何かをする
}

std::unordered_mapのkeyはconstであるという仕様のため、std::pair<const std::string, int>が正しい。
上記の例のようにconst std::pair<std::string, int>&としてしまうと、コンパイラはstd::pair<const std::string, int>からstd::pair<std::string, int>の一時オブジェクトを生成し、それをconst参照にバインドする、という無駄な処理を行ってしまう。
以下のように記述すればこのミスを防げる。

for (const auto& p : m)
{
    ...
}

メリット4とメリット5はどちらも暗黙の型変換ため気づきにくいミスをautoが防いでくれるという例。

以上のようにautoを使用するメリットは複数あるが、いつでもautoさえ使っておけば安心というわけではない。
その例は、Item 2 とItem 6で記述されている。

中にはautoが変数の型を分かりにくくすると主張する人がいる。
しかし、IDEを使う場合は簡単に型を表示できるし、だいたいの型でよければ、変数名を賢く設定すれば分かるものだ。
また、ソフトウェア開発コミュニティではC++以外の型推定を持つ言語や動的型付け言語の実績が数多くあり、商業規模のコードの信頼性や保守性にも十分耐えられると言える。

メリット6. リファクタリングが容易になる

autoを使用している場合、初期化時の型を変更すればautoの部分は自動的に変更が伝播するので、1つの型を変更することで何箇所も修正するという手間が無くなる。

Things to Remember

  • autoは初期化忘れを防ぎ、型指定のミスによる移植性や効率の問題を解消し、リファクタリングを用意し、タイプ数を削減する。
  • ただし、autoの落とし穴には注意する必要がある(Item 2と Item 6で記述)