2020年5月20日水曜日

C++ Templates - The Complete Guide, 2nd 読書メモ 2章 クラステンプレート

C++ Templates - The Complete Guide, 2ndの2章「クラステンプレート」の読書メモ

スタックの実装を例にクラステンプレートについて解説している.


クラステンプレートの宣言


関数テンプレートと同様に,templateの後にテンプレート仮引数を並べ,クラス宣言を書く.

template <typename T>
class Stack {
  ...
};

クラステンプレート内では,Tを型として利用できる.

コピーコンストラクタなどを書く場合,次の2通りの書き方があるが,敢えて書くと意味があるように思わせるので,前者の方が良い.

Stack(const Stack &);
Stack(const Stack<T> &);

メンバ関数の実装


クラステンプレートのメンバ関数を実装する方法として,クラスの外側で定義する方法とクラス内で定義する方法が紹介されている.

ついでに,例外安全についても触れていて,Exceptional C++が紹介されている.

クラステンプレートの利用


基本的には,クラステンプレートを利用する場合は型を明示する必要があるが,C++17では型を省略する方法が追加され,後ほど解説する,とある.

また,呼び出されたテンプレート(メンバ)関数のみがインスタンス化される,とある.

friend


フレンド関数を定義するには,クラス内で宣言してしまう方法と,後から別途宣言する方法が2つ紹介されている.

template <typename U>
friend std::ostream & operator<<(std::ostream &, const Stack<U> &); // 別途非メンバ関数として定義する

template <typename T>
class Stack; // 前方宣言
template <typename T>
std::ostream & operator<<(std::ostream &, const Stack<T> &);

template <typename T>
class Stack {
    friend std::ostream & operator<< <T>(std::ostream &, const Stack<T> &);
};

テンプレートの特殊化


template <>として,クラス名の後に具体的な型を指定して定義することで,特定の型の場合について専用の実装を用意することができる.

例としては,std::vector<bool>など.

例えば,typename Tに対して,T *,とポインタ型にするような部分的な特殊化も可能.

デフォルト実引数


関数テンプレートと同様に,テンプレート仮引数にデフォルト実引数を指定することが可能.

タイプエイリアス


typedefのように型の別名を定義する方法として,usingによるエイリアス宣言が紹介されている.

typedefよりも = でつなぐ分,読みやすい,とも.

また,エイリアスはテンプレートにもできる.

template <typename T>
using DequeStack = Stack<T, std::deque<T>>;

C++14以降では,標準ライブラリでは 型とレイトである型から他の型を取り出す場合,今まではA::typeとしていたのを,A_tというようなエイリアスが用意された.

クラステンプレート実引数推論


C++17から,初期化時の実引数からクラステンプレートの型を推論できるようになった.使い方次第では便利なのだろうけれど,使いこなせない気がする.

文字列を利用して推論する場合の注意点についても触れている.コンストラクタが参照を受け取るようになっていると,文字列から推論しようとするとconst char [x]のような型になってしまうが,値渡しにしていると,const char *になる.

推論ガイド(deduction guide)についてもサラッと触れている.

テンプレート化された集成体


ユーザー定義または継承したコンストラクタを持たず,privateやprotectedな非staticメンバを持たず,仮想関数,仮想またはprivateまたはprotectedなベースクラスを持たないクラスまたは構造体をテンプレートにすることもできる.

2020年5月7日木曜日

C++ Templates - The Complete Guide, 2nd 読書メモ 1章 関数テンプレート

C++ Templates - The Complete Guide, 2ndの1章「関数テンプレート」の読書メモ

関数テンプレートとは

int max(int, int);
double max(double, double);
string max(string, string);

このように,型が違うだけで同じような関数をまとめて,次のように型名をtypenameで仮引数として定義できる.

template <typename T>
T max(T, T);

family of functions,という表現がポイントなのかな,と.

この関数テンプレートは,次のように呼び出せる.

max(1, 3);
max(1.0, 3.0);

このとき,具体的にTにintやdoulbeがあてはめられて関数が生成される.具体的な関数が生成されることをインスタンス化(instantiation)と呼び,生成された関数をテンプレートのインスタンス(instance)と呼ぶ.

Two-Phase Translation

テンプレートは,定義をしたタイミングと,具体的な型が割り当てられたタイミングの2つのタイミングで解釈が行われる.例えば,上記のmaxであれば大小比較が必要なので,大小比較が定義されていない型が渡されると,インスタンス化のタイミングでエラーとなる.

一方で,テンプレートの定義時点で定義されていない型名が関数テンプレート内で利用されている,といった文法上のエラーなどがある場合に,仕様上はエラー判定が行われるのだけれど,Microsoft Visual C++はその辺が緩い.Clangだとちゃんとエラーになる.その辺の挙動を切り替える方法については,次のリンク先にまとめてある.


型推論時の型変換

関数テンプレートは,呼び出し時に渡された実引数から型を自動的に決定する.これを型推論(Type Deduction)と言う.

この型推論が行われる際には,型変換が制限される.例えば,doubleを受け取る関数にintの値を渡しても,普段であれば暗黙の型変換が行われるが,関数テンプレートの場合,エラーになる.
max(1, 3.0); // コンパイルエラー

これを回避するには,明示的にキャストをするか,関数テンプレートの型を直接していする.

max(1, 3.0); // T=doubleとして判定される.

ちなみに,必要性が分からないのだけれど,デフォルト実引数を指定していたとしても,デフォルト実引数の型をもとに型推論は行われない,とのこと.

複数のテンプレート仮引数


標準ライブラリのmaxはそうなっていないけれど,説明のためにmaxのテンプレート仮引数を複数にして,複数の型を受け取れるようにしよう,という説明が進む.

こうすると,今度は返り値の型はどちらにそろえるんだ,といった問題が発生する.

そこで,返り値の型を推論する方法が示される.(書籍よりコードを抜粋)

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> std::decay_t<decltype(true ? a : b)>
{
    return b < a ? a : b;
}

返り値の型を指定するところにautoを書いて,後ろにアロー(->)を書いて,decltypeで式の結果の型を求める.結果が参照などでは困る場合があるので,std::decay_tを利用して返り値の型に適切な型に変換する.

次のように,std::common_type_tを利用する方法も示されている.

template <typename T1, typename T2>
std::common_type_t max(T1 a, T2 b)
{
    return b < a ? a : b;
}

あくまでも説明のために出しているだけで,実際にこれをやると,intとdouble,doubleとint,のように型がひっくり返っているだけでも別の関数が生成されてしまい,プログラムサイズが無駄に増えるので望ましくないと思われる.

ちなみに,a > bとしないのは,C++の仕様を見るに,基本的には < を基準に比較演算が考えられているからだと思われる.

デフォルトテンプレート実引数


通常の関数と同様に,テンプレート仮引数にもデフォルト実引数を指定することができる.ただし,関数テンプレートにデフォルトテンプレート実引数を指定できるようになったのは,C++11以降なので注意が……今時必要ない気がするけど,未だにC++11さえも使えない環境もあるかもしれない.そうだとすると相当ツライ……

関数テンプレートのオーバーロード


通常の関数と同様に,関数テンプレートもオーバーロードできる.これ,意外と驚かれる.通常の関数もオーバーロードに含めた場合,どちらが優先されるかなどについては,13章で詳しく扱うようだ.第一版では9章だったので,4章ほど追加されているのかな.

その他


この単純なmax関数だけでも,色々考えることがあって,値渡しと参照渡し,どちらが良いのか,inlineにはできないのか,constexprは,など色々考えることがあるよ,と.

2020年5月6日水曜日

MSVCとClangにおけるテンプレートの名前の解釈の切り替え

Clangのドキュメントによると,Microsoft Visual C++では,テンプレートがインスタンス化されるまでインラインメソッドの本体の解析が行われないが,clangは行う.これだと,Windows系のヘッダファイルの解析時に問題になるので,-fdelayed-template-parsing,というオプションを指定することで,VC++と同じ挙動に出来る.

つまり,次のようなコードのコンパイルが通るようになってしまう.

template  void f(Type t) {
    Typo t; // 敢えてタイプミス
}

int main()
{
    // fのインスタンス化はしていない
}


一方で,VC++をClangと同じようにする方法がある.プロジェクト設定のC/C++の言語から,準拠モードを「はい(/permissive-)」にすれば良い.

詳細についてはC++ Team Blogで紹介されている.

MS Docsによれば,Visual Studio 2017 15.5以降ではデフォルトでこの設定が「はい」になるとのこと.逆に言えば,それより前から継続して使っているプロジェクトには設定されていないということなので,何かのきっかけで新しいプロジェクトを追加した際などには,この設定の差が問題になるかもしれない.