C++11 型推論(auto, decltype)の徹底活用

C++11, 14 により C++ でも 型推論(auto) が使えるようになりました。 C++11 の機能の中でも型変換は手っ取り早くコードを書くのが楽になるので、 使っている人は多いと思います。
今回はその型推論をもっと徹底的に使っていこうというお話です。

型推論とは

C++ のような静的型付けの言語では変数の型はきっちり指定しておく必要があります。 いちいち書くのはめんどくさいですが、 コードの安全性や実行プログラムの速度に効果があります。
一方、Ruby や Python のような動的型付け言語では、 変数自体が型情報を持っていて、型を指定する必要がなく楽ちんですし、 配列に任意の型を入れられるといった柔軟性があります。
これはどちらが優れているというものではなく、言語には適材適所があるということです。

とはいえ、めんどくさいものはめんどくさいので、「少しでも楽をしよう」というのが型推論です。
型推論では変数の型を与えられた初期値から推論します。 それを使って、型を書くのを省略できるところは省略してしまおうという方法です。 C++ の型推論では型宣言に auto と書きます。 それをコンパイラーが初期値から推論して型を決めてくれます。

型推論はどこまで使えるのか

型推論はローカル変数以外にも使えます。 対象をまとめると次のようになります。

対象 対応
ローカル変数
グローバル変数 ×
クラスのメンバー変数 ×
クラス変数(static) const のみ可
関数の戻り値 仮想(virtual)関数以外 (C++14)
関数の引数 ラムダ式のみ可 (C++14)


C++14 は C++11 の補足的な変更なのですが、C++14 で結構広がりました。 特に「関数の戻り値」の型推論は意外な実力を秘めています。
それを含め次章から個々の型推論について説明してきます。

ローカル変数

通常ローカル変数

関数や制御文のブロック内での変数で、型推論として一番よく使う場面だと思います。
int foofunc()
{
  auto localver = 1;  
const やポインター(*)、参照(&)等に変えたい場合、修飾子は別途付ける必要があります。 (もともとポインターであれば auto でポインター)

リテラルの工夫

文字列リテラルを初期値として渡す場合は型推論では const char * になります。(環境によって char * かも)
しかし std::string にしたいという時もあると思います。 そういった場合には C++14 で追加された basic_string のリテラルを使うと出来ます。
  auto cstr = "Hellow";  // const char *
  auto str  = "Hellow"s; // std::string  
そのほかのクラスでよく使うものがあれば、C++11 では自作のリテラルを定義することもできるようになっています。

範囲 for

明示的に初期値を与えているわけではありませんが、C++11 の 範囲 for 文 でも型推論は使えます。
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (const auto &elem : strs) {
    cout << elem << endl;
  }  

グローバル変数

グローバル変数の場合には extern 宣言を必要とするため、使うことはできません。
extern int64_t ErrNo;  
ファイルローカル、逆に言うとファイル内でグローバルな static 変数に関しても、auto が使えます。
static auto stvar = 1.0;
static を外したものは分類としては名前空間スコープ変数に入り、auto は使えます。 そのままだと普通は使いませんが、const を付けて定数として公開することはあります。
const auto VERION = "1.0.0";  

クラス関連の変数

クラス変数 (静的メンバー変数)

クラス定義で static を付けると静的メンバーとなり、変数はクラス変数となります。 クラス変数は const の場合、初期値を指定することができます。
class Foo
{
  static const int Dim = 2;
  // static int Dim = 2; NG const のみ OK
  static constexpr double Mag = 0.5;
const が付けられるのは int のみです。C++11 では constexpr を付けると他の型でも初期値が指定できるようになっています。 const ではないクラス変数では初期値を指定できないので auto は使えませんが、 上記のようなクラス定数であれば auto が使えます。
class Foo
{
  static const auto Dim = 2;
  static constexpr auto Mag = 0.5;

インスタンス変数 (メンバー変数)

メンバー変数でいえば、C++11 からクラス変数ではない通常のメンバー変数(インスタンス変数)でも初期値を指定できるようになりました。 初期値を指定できるので auto も使えるのではないかなと思ったのですが、auto の対象外っぽいです。
class Foo
{
  /// メンバー変数
  int m_bar = 0;
  // auto m_baz = 0; NG

関数の戻り値

戻り値の型推論

C++14 から戻り値の型推論もできるようになりました。
例えば次の関数では戻り値は a または b でともに T なので、 コンパイラーが T と確定します。
template <typename T>
auto StdMax(const T &a, const T &b)
{
  return (a < b) ? b : a;
}  
  cout << StdMax(4, 12) << endl;  // 12
戻り値の型推論は通常関数、クラスのメンバー関数にかかわらず使えます。 ただし、 virtual なメンバー関数の場合は auto は使えません。 これは、オーバーライドのチェックと仮想関数テーブルのレイアウトが複雑になるためらしいです。

型が確定しない戻り値 : 三項演算子

前節の StdMax() は std::max() とほぼ同じなのですが、 整数のリテラルと int64_t や double を比較するとコンパイルエラーになるので、使いづらいです。 そこで、引数の型を別々にできるようにしてみます。
 template <typename T, typename U>
 auto MyMax(const T &a, const U &b)
 {
   return (a < b) ? b : a;
 }
こうすると、引数によって戻り値の型がかわる関数を作成することができます。
  cout << MyMax(4.2, 12) << endl;  // 12
  cout << MyMax(4.2,  2) << endl;  // 4.2  

型が確定しない戻り値 : 複数の return 文

複数のリターン文で戻り値の型が確定しない場合には型推論はできません。 前節の関数を if 文を使うように直してみます。
template <typename T, typename U>
auto MyMax2Return(const T &a, const U &b)
{
  if (a < b) {
    return b;
  }
  else {
    return a;
  }
}  
これだと MyMax2Return(4.2, 12) のように T, U が別々の型になる場合にはコンパイルエラーとなってしまいます。
  cout << MyMax2Return(4, 12) << endl;  // 12
  // cout << MyMax2Return(4.2, 12) << endl;  // コンパイルエラー
「さっきはできたなのになんで」って思いますが、 どちらかというと 出来る方が C++11 の仕様的にいいのかな という感じです。
これに限って言えば三項演算子に戻せばいいのですが、 「どうしても複数の return 文する必要がある」として解決策を考えてみます。

どうすれば解決できるかというと、C++11 で追加された式の型を取得する decltype後置の戻り値型宣言を使います。
template <typename T, typename U>
auto FixedMyMax2Return(const T &a, const U &b) -> decltype(a+b)
{
  if (a < b) {
    return b;
  }
  else {
    return a;
  }
}  
"a+b" の型というのは、例えば double と int との変数を足すと double になります。 基本型の四則演算の結果のように新しい型を決めてその値を返します。
  cout << FixedMyMax2Return(4.2, 12) << endl;  // 12.0
  cout << FixedMyMax2Return(4.2,  2) << endl;  //  4.2  

decltype の活用

decltype を使えば、今まで出来なかったようなことができるようになります。

例えば、以下のような点クラス(struct)があったとします。
template <typename T>
struct Point
{
  T x;
  T y;
};  
このクラスの掛け算(*)の演算子をオーバーロードしたとして、次の計算をします。
  Point<int>(3,2) * 0.5 
戻り値をしては Point<double>(1.5,1.0) のような double の点を返してくれるのが一番自然だと思います。
しかし、今まではこれを実現するのは不可能でした。 正確には基本型の全種別を個別に定義していけば可能ですが、現実的ではありません。

これが decltype と戻り値の型推論を使えば、基本型のように型を変えるクラスを簡単に定義できます。
template <typename T, typename U>
inline auto operator*(const Point<T> &a, U b)
{
  return Point<decltype(a.x*b)>(a.x*b, a.y*b);
}
  cout << Point<int>(3, 2) * 0.5 << endl;  // Point<double>(1.5, 1.0)
ちなみに decltype と後置の宣言でも定義可能です。
template <typename T, typename U>
inline auto operator*(const Point<T> &a, U b) -> Point<decltype(a.x*b)>
{
  return {a.x*b, a.y*b};
}

関数の引数

引数宣言の auto

関数の引数の宣言に auto を使うことを考えてみます。
先ほどの点クラスに引数、戻り値ともに auto で指定したとします。
template <typename T>
struct Point
{
    :
  auto Plus(auto val) const
  {
    return Point(static_cast<T>(x+val), static_cast<T>(y+val));
  }
これは初期値から推測する型推論ではなくなっているのに気付かれるでしょうか。 こうなってくるとテンプレートを簡略化した 自動ジェネリック というものになってきます。

ただ、残念ながらこれはコンパイルできるようになっていません。 しかし、仕様上は OK なのかはわかりませんが、 戻り値の型推論をしないと gcc ではコンパイルが通ります。
  // 引数だけ auto
  Point PlusAutoArg(auto val) const
  {
    return Point(static_cast<T>(x+val), static_cast<T>(y+val));
  }
前章と同様に戻り値だけ auto も出来ます。
  // 戻り値だけ auto
  template <typename U>
  auto PlusAutoReturn(U val) const
  {
    return Point<decltype(x+val)>(x+val, y+val);
  }

ジェネリックラムダ (C++14)

C++14 からラムダ式に限り、 引数、戻り値ともに auto を指定できるようになっています。
正確にいうともともとラムダ式は戻り値は記述せずに型推論し、 C++14 から引数宣言で auto が使えるようになりました。
  auto plus = [](auto a, auto b) { return a + b; };
  cout << plus(2,   3) << endl; // result = 5
  cout << plus(2.5, 3) << endl; // result = 5.5  
これまた、「これが大丈夫なのに、さっきのはなんでダメなの」という気はします。 ただ、ラムダ式のように関数内に書くのに template 宣言は書けないですし、 頑張って仕様として入れたのではないかと思います。

サンプルコード

今まで出てきたコードのファイルもあげておきます。


スポンサーサイト
 

C++ での Mixin の活用 : Comparable を使って比較演算子を簡単実装

オブジェクト指向プログラミングには Mixin という手法があります。 これを使えば、自作のクラスを比較可能な(Comparable)クラスにすることが簡単に出来ます。 今回は Comparable Mixin を例に C++ での Mixin のやり方について説明します。

Mixin とインターフェース

Mixin の前提として、 多重継承の問題点を把握しておく必要があります。 その辺の話や Mixin の概要に関しては以下の記事をみていただくとして、 C++ に限定して簡単に説明していきます。 多重継承の問題を避けるための代表的な方法はインターフェースです。
class IComparable
{
 public:
  virtual int Compare(const IComparable &other) const = 0;
};
インターフェースの制限は次の 2 つです。 この制限によりインタフェースクラスは多重継承しても問題が発生しないので、クラスに対して自由に継承させられます。
  • 属性(メンバー変数)を持たない
  • 操作(メンバー関数)は宣言のみで実装を持たない(純粋仮想関数)
インターフェースを使うことにはいろんなメリットがあります。
  • 利用側 : 操作(Compare) を持つことが保証される
  • 継承側 : 操作(Compare) の実装が強制される
例えば、ソートのような処理のインプットで ICompareble を指定すれば、 Compare() が使えることが保証されますし、 実際に ICompareble を継承するクラスでは Compare() を実装しないとコンパイルエラーとなります。

cpp_mixin_interface.png


インターフェースはインターフェースで有用です。 ただ、 多重継承で問題になるのは属性を持つことの方で、 操作の実装を禁止する必要はありません。
「操作の実装を持つ」ということは言い換えるとなんらかの「機能を持つ」ということです。 そういったクラスを継承すると元のクラスに機能を付加することになります。 継承によって機能を追加するためのクラスやその手法を Mixinと呼びます。

例えば、『鯨』というクラスを考えてみます。
ほ乳類、魚類から継承したとするとダイヤモンド継承(ひし形継承)と呼ばれる共通の祖先を持つクラスの継承となり、多重継承で最も問題になる形になります。

ダイヤモンド継承

Mixin を用いると「泳げる」(Swimable) という機能を共に持つとみなすことができます。 ダイヤモンド継承は避けられますし、こちらの方がより自然にクラスによるモデル化ができています。

Mixin

Comparable Mixin

Mixin をさらに Comparable(比較できる) Mixin を具体例として説明していきます。


次のような点クラスについて考えてみます。
ちなみに struct はデフォルトが public なことを除いて class と同じものです。
struct Point
{
  int x;
  int y;
};
点クラスのように 等比、比較演算子のオーバーロードを行うと使いやすくなるクラスは多いと思いますが、 毎回それらを実装するのは面倒です。
しかし、 それらの演算子は比較関数から導くことが出来ます。 これを実装したクラスが Comparable Mixin のクラスです。
template <class T>
class ComparableMixIn
{
 public:
  /// 比較用関数
  ///
  /// | 状態               |  戻り値  |
  /// |--------------------|----------|
  /// | other の方が大きい | 負の値   |
  /// | other と一致       |    0     |
  /// | other の方が小さい | 正の値   |
  /// 
  virtual int Compare(const T &other) const = 0;


  // 演算子の実装
  bool operator==(const T &other) const { return Compare(other) == 0; };
  bool operator!=(const T &other) const { return Compare(other) != 0; };
  bool operator< (const T &other) const { return Compare(other) <  0; };
  bool operator<=(const T &other) const { return Compare(other) <= 0; };
  bool operator> (const T &other) const { return Compare(other) >  0; };
  bool operator>=(const T &other) const { return Compare(other) >= 0; };
};
Mixin クラスの特徴は次のようなものです。 インターフェースと同様に属性を持たないので多重継承ができますが、 機能(操作の実装)を持っているところが違います。
  • 属性を持たない
  • キーとなる関数の宣言(純粋仮想関数)
  • キー関数を使って機能となる関数を実装
ここではキーとなる関数は Compare() です。 この Compare() を使って、 比較できる(Comparable)という機能、 すなわち 等比や比較演算子のオーバーロードの実装を行っています。


一方、使う側である点クラスでは ComparableMixIn を継承し、 Compare() を実装します。
ここには実装のちょっとしたテクニックがあります。 ComparableMixIn は継承先の型をとるテンプレートです。こうすることによって継承先の型による演算子のオーバーロードを可能にしています。
struct Point : public ComparableMixIn<Point>
{
   :

  /// 比較関数.
  /// x で比較し、同じなら y で比較
  virtual int Compare(const Point &other) const override
  {
    return ((x != other.x) ? (x - other.x) : (y - other.y));
  }
};
cpp_mixin.png

この Mixin によって、 点クラスは比較の機能をもつことができるようになります。
  Point a(1, 2), b(2, 3);

  cout << boolalpha;
  cout << "a == b : " << (a == b) << endl;
  cout << "a != b : " << (a != b) << endl;
  cout << "a <  b : " << (a <  b) << endl;
  cout << "a <= b : " << (a <= b) << endl;
  cout << "a >  b : " << (a >  b) << endl;
  cout << "a >= b : " << (a >= b) << endl;
実行結果 :
a == b : false
a != b : true
a <  b : true
a <= b : true
a >  b : false
a >= b : false
なお、Boost::Operators を使うと == から != 、 < から他の不等号を定義できます。 厳密に言うと < さえ定義されていれば、 すべての等、不等号 を導くことができます。 (a == b は a < b && b < a)
これは < 演算子をキー関数とした Mixin ということもできます。

ソースファイル

以上のように Comparable Mixin を使うと Compare() を実装するだけで、等号、不等号の機能を簡単に追加することができます。
大したコード量ではありませんが、ヘッダーを公開していますので、ダウンロード(リンク先を保存)して自由に使ってください。[MIT ライセンス]



 

C++11 範囲に基づく for 文

多くのスクリプト言語では for in の形式でコンテナーの要素にアクセスできます。 Boost や Qt などでは C++ でもなんとかそれを実現させようとマクロを駆使して for_each として定義されていました。
しかし、とうとう C++11 で範囲 for は言語機能として正式に追加されました。
今回はそんな 範囲 for について紹介します。

範囲 for とは

範囲 for というのは C++11 で The range-based for statement(範囲に基づく for 文) として規定されたもので、 配列、リストといったコンテナーの各要素に順にアクセスする for です。


プログラミング中では、配列などのコンテナーに対してループを回して要素にアクセスする というのはよく行う処理です。
その際、 今まではループカウンターを回したり、イテレーターを使っていたと思います。 範囲 for を使えば次のように書けます。
  vector<int> ary = { 3,2,9,6 };

  for (auto elem : ary) {
    cout << elem << endl;
  }  
ループカウンターやイテレーターを書く必要がなく、簡単に書けるようになります。 すっきりしたので、 "コンテナーの要素にアクセスしている" というコードの意図が分かり易くなるという利点もあります。
また、添え字アクセスと違い、イテレーターのようにランダムアクセス以外にも使える汎用性があります。

なお、この考えを一歩進めると関数型のデータ処理となり、 for の逐次処理以外のいろんな処理ができるようになります。

範囲 for の対象

範囲 for に使えるコンテナーは次の 3 つです。
  • STL のコンテナーなど begin(), end() メソッドを持つクラス
  • 配列
  • 初期化リスト
前章のコンテナー以外もサンプルをあげておきます。

配列:
  int cary[] = { 3,2,9,6 };
  
  for (auto elem : cary) {
    cout << elem << endl;
  }
初期化リスト:
  for (auto elem : { 3,2,9,6 }) {
    cout << elem << endl;
  }

要素の型

範囲 for の要素(elem)の定義にはコンテナーの要素の型を記述します。 これには C++11 に型変換として追加された auto が使えます。

その際、関数の引数のようにコンテナーに変更を加えるかどうかによって、 参照(auto&)か const 参照(const auto &)になります。

変更を加える:
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (auto &elem : strs) {
    elem += "!";
  }
  // { "foo!", "bar!", "baz!" }   
変更を加えない:
  vector<string> strs = { "foo", "bar", "baz" };
  
  for (const auto &elem : strs) {
    cout << elem << endl;
  }  
ただし、最初のサンプルのように要素が int などサイズが小さい場合は、 コピーしても問題がないため、ただの auto にします。
また、参照ではなく、ポインターを指定することも出来ます。

std::map の場合

Boost の for_each の時は std::map だとちょっと面倒でした。 範囲 for の場合は問題なく使えます。
  map<string, int> strmap = {
      { "foo", 3 },
      { "bar", 2 },
      { "baz", 9 }
  };
  for (const auto &elem : strmap) {
    cout << elem.first << " - " << elem.second << endl;
  }
std::map の場合の要素は std::pair です。
範囲 for の対象は begin(), end() を持つ必要がありました。 すなわち、各要素にはイテレーターを使ってアクセスしています。
前章では要素の型をいくつか紹介しましたが、 「イテレーターから導けるものが型として使える」と言えます。

範囲 for は次の記述を簡略化したものと考えると分かりやすいと思います。
  for (auto itr = ary.begin() ; itr != ary.end() ; itr++) {
    auto elem = *itr;
    // 要素に対する処理
  }  

サンプルコード

記事中で紹介したサンプルコードのファイルです。


 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
60位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
5位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

03月 | 2017年04月 | 05月
- - - - - - 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 - - - - - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。