あめだまふぁくとりー

Boost.Graphとかできますん

名前解決から始める C++

この記事は 初心者 C++er Advent Calendar 2015 14 日目の記事です.

いきなりですが次のコードを見てください.

#include <iostream>

struct cat
{
  std::string name;
};

void print_name(cat const& neko) // const 参照渡し!
{
  std::cout << "this cat name is " << neko.name << std::endl;
}

int main()
{
  cat neko{"katsuo"}; // 波括弧初期化!!

  print_name(neko);
}

const 参照渡しと波括弧初期化はそれぞれ 8 日目2 日目の記事で説明があったので, ここまでの記事を読んだ人なら上記のコードはわかるはずです (struct cat をちゃんとしたクラスにしようと思いましたけど長くなるのでやめました. クラスについては 5 日目の記事を参照しくてださい).

ではここで問題です. このコードを実行すると何が起きるでしょうか?

さっそく回答です.

this cat name is katsuo

どうですか? 難しかったでしょうか?

問題 2

それでは続いて次のコードです.

#include <iostream>

namespace animals { // 名前空間

  struct cat
  {
    std::string name;
  };

  void print_name(cat const& neko) // const 参照渡し!!
  {
    std::cout << "this cat name is " << neko.name << std::endl;
  }

} // namespace animals

int main()
{
  animals::cat neko{"yamada"}; // 波括弧初期化!!!!

  print_name(neko); // おや? この呼び出しは?
}

struct catvoid print_name(cat const&)animals という名前空間に入れました.

それではこのコードを実行すると何が起きるでしょうか? と, その前に名前空間について少しだけ説明しましょう.

名前空間とは?

変数や関数等の名前をまとめる機構です. クラスでも名前をまとめることができますが, クラスの場合はそのクラスの役割に必要なものだけをまとめるのが普通です.

一方, 名前空間はもっと緩い意味で名前をまとめます. 例えば, 標準ライブラリの std のように特定のライブラリをまとめたものだったり, 関数の内部詳細をまとめたものだったりします.

名前空間の中と外ではスコープが違うので同じ名前のエンティティを定義できます.

int g; // グローバル変数の定義

namespace ns {

  int g; // スコープが違うので同じ名前の変数を定義できる

}

名前空間の中の名前を参照する場合はスコープ解決演算子 :: を使用します. これは <名前空間の名前>::<参照されるエンティティの名前> のように書きます.

namespace ns {

  void f(int i) {}

}

int main()
{
  f(3); // コンパイルエラー! X(
  ns::f(3); // OK! :)
}

問題 2 のコードでも main 関数から struct cat を参照する場合は animals::cat とスコープ解決演算子を使用していますね.

コードの続き

名前空間がなんとなくわかったところで先ほどのコードの続きです.

int main()
{
  animals::cat neko{"yamada"}; // 波括弧初期化!!!!

  print_name(neko); // おや? この呼び出しは?
}

このコードを実行すると何が起きるでしょうか? そもそもコンパイルできるのでしょうか? 名前空間を理解したので楽勝ですね. 試してみましょう.

melpon.org

どうですか? 期待通りの答えだったでしょうか?



いろいろ言いたいことはあるかもしれませんが次に進みます.

問題 3

#include <iostream>

int main()
{
  std::cout << 'A';
}

今度のコードはとてもシンプルですね.

このコードを見た人は「答えは A だ」と即答するかもしれません.


残念違います.


誰もこのコードを実行したら何が起こるかなんて聞いていません. 焦りは禁物です.

質問はこうです.

「このコード上に現れる << とは何か」.

あ, 哲学的に考える必要はありませんよ.

演算子オーバロード

std::coutstd::ostream 型を持つオブジェクトです. これにちなんで << をストリーム演算子と呼ぶ書籍もあったりします (多分).

というわけで <<演算子っぽいです. これはかなりいい線いっています. std::cout'A' を整数に置き換えてみましょう.

int main()
{
  8 << 2;
}

あ, これシフト演算子.

正確には上記コードの << は右シフト演算子演算子オーバロードした関数です.

この関数は以下のように定義されています (いろいろ正確でない部分がありますがここではこれで十分です).

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

演算子 @ の使用は operator@ の呼び出しに置き換えられます (ここで @ は何かの演算子).

問題 4

上記のオーバロード演算子の定義と質問のコードとつなげてみましょう.

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

int main()
{
  std::cout << 'A';
}

そしてシフト演算子を置き換えると関数の呼び出しに置き換えると...

namespace std {

  ostream cout;

  std::ostream& operator<<(std::ostream& os, char c)
  {
    // ... output c to stdout
  }

}

int main()
{
  operator<<(std::cout, 'A'); // 置き換え!
}

どことなく問題 2 のコードに似ていますね. それでは最後の問題です.

「問題 3 のコードでなぜ 'A' が出力されると思った?」

置き換え後は, std::operator<<(std::cout, 'A') ではなく operator<<(std::cout, 'A') になります. なぜなら, << のどこにも std なんて付いていないのですから.

このコードはまちがいなく問題 3 のコードと等価ですが, 名前空間の修飾なしに関数呼び出しを行っています.

つまり問題 2 のコードがコンパイルエラーになると思うのなら, 問題 3 のコードもコンパイルエラーになると思うべきなのです.

ADL (Argument-Dependent name Lookup)

この謎を解くのが ADL です.

ADL とは簡単にいうと, 引数の型と同じ名前空間にある関数も呼び出しの対象になりますよというものです.

問題 2 のコードでは print_name の引数の型は animals::cat です. なので ADL によって名前空間 animals 内の print_name も呼び出し対象に含まれます.

問題 4 のコードでは引数の型は std::ostreamchar でした. よって ADL によって名前空間 std 内の operator<< も呼び出し対象になるのです.

これが上記二つのコードがコンパイルエラーにならず実行できた理由です.

ちなみに組み込み型には名前空間がないので char 型の引数は特に影響しません.

また, ADL が行われるのは関数の名前にスコープ解決演算子が付いてないときだけです.

namespace animals {

  struct cat;

  void print_name(cat const& neko);

}

namespace ns {

  void print_name(int a); // 引数の型は int

}

void print_name(int a); // 引数の型は int

int main()
{
  animals::cat neko{"norisuke"};

  ns::print_name(neko); // コンパイルエラー!! ADL なし

  ::print_name(neko); // コンパイルエラー!! この場合も ADL なし

  print_name(neko); // OK! ADL あり
                    // 引数の型から ::print_name ではなく
                    // animals::print_name を呼び出す
}

このように C++ では一見コンパイルエラーに見える関数呼び出しも ADL によって問題なく実行できる場合があります.

問題 4 で示した通り, 演算子オーバロードを自然な形で呼び出すことができるのは ADL のおかげなのです.

ADL によるアルゴリズムのカスタマイズ

ここからは中級者への一歩. ADL を使ったアルゴリズムのカスタマイズを見ていきます.

以下のコードを見てください.

namespace others_libs {

  // 座標取得関数
  template <class Vec>
  double get_x(Vec const& v) { return v[0]; }
  template <class Vec>
  double get_y(Vec const& v) { return v[1]; }

  // 二つのベクトルのなす角を計算
  template <class Vec>
  double angle(Vec const& v1, Vec const& v2)
  {
    double const x1 = get_x(v1);
    double const y1 = get_y(v1);
    double const length1 = std::sqrt(x1 * x1 + y1 * y1);

    double const x2 = get_x(v2);
    double const y2 = get_y(v2);
    double const length2 = std::sqrt(x2 * x2 + y2 * y2);

    return std::acos(
      (x1 * x2 + y1 * y2) / (length1 * length2));
  }

}

これは誰かが提供してくれた二つのベクトルのなす角を計算するライブラリです.

このライブラリではベクトルの各座標は配列のようにインデックスでアクセスできることを想定してます. なので, ベクトルを表現する型が配列や std::vector などインデックスアクセスがサポートされる任意の型で使用可能です.

ベクトルといえば 5 日目 の記事で Vec2 クラスを定義しました.

namespace my_libs {

    struct Vec2
    {
        double x;
        double y;

        // ... そのほかの関数定義
    };

}

ここでは話を進めやすくするために Vec2my_libs という名前空間の中に入れました.

ベクトルを使用するなら配列よりも, ベクトルとしてきちんと定義した型を使用したいですね. しかしながら, Vec2 はインデックスアクセスをサポートしていないため others_libs::angle と組み合わせることができません.

Vec2 をインデックスアクセスできるように変更すべきなのでしょうか?

いいえ. この問題を ADL が解決してくれます.

namespace my_libs {

    struct Vec2
    {
        double x;
        double y;

        // ... そのほかの関数定義
    };

}

namespace my_libs {

    // Vec2 と同じ名前空間に定義
    double get_x(Vec2 const& vec) { return vec.x; }
    double get_y(Vec2 const& vec) { return vec.y; }

}

Vec2 と同じ名前空間に関数を追加するだけで others_libs::angleVec2 に対して使用するようにできました *1.

この方法の凄いところは, Vec2 にも others_libs::angle にも一切変更を加えていないということです. つまり, どちらのソースも変更できない場合でもこの方法は適用可能なのです.

アダプタ関数

ここで注目すべきなのは others_libs::angle 中の get_xget_yアダプタとして機能していることです.

これらの関数の呼び出しではスコープ解決演算子を使用していません. つまり, ADL を利用してカスタマイズすることを想定しているのです *2.

このように, ADL はアルゴリズムのカスタマイズポイントとして利用ができます.

この ADL によるカスタマイズポイントは C++11 以降の範囲 for ループや STL, Boost ライブラリ等広く利用されています. そしてその際たる例が, 拡張可能なグラフライブラリとして設計された Boost.Graph なのです!! (本記事の目的達成).

まとめ

本記事ではもともとは shared_ptr のコストについて書く予定だったのですが全然初心者っぽくなかったので, Boost.Graph の宣伝も兼ねて ADL の説明をしました.

ADL にはプラスの面だけでなくマイナスの面もありますが, それは誰かが説明してくれるでしょう.

ADL についてより理解を深めたい方は Exceptional C++ の第 5 章も読むとよいと思います *3... と思ったらこの本絶版? 丸善さん再出版お願いしますー.

Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)

Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)

  • 作者: ハーブサッター,浜田光之,Harb Sutter,浜田真理
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2000/11
  • メディア: 単行本
  • 購入: 9人 クリック: 134回
  • この商品を含むブログ (63件) を見る

追記: 上記商品紹介中の Herb Sutter 氏の英語スペルが Harb Sutter になっていますが, 正しくは Herb Sutter です (@kariya_mitsuru さんご指摘ありがとうございます!!).


この記事は 初心者 C++er Advent Calendar 2015 14 日目の記事でした.

*1:Vec2 がグローバル名前空間に定義されている場合, 同様にグローバル名前空間に関数を追加すれば ADL が機能します.

*2:逆にカスタマイズを想定しない場合は名前空間で関数名を修飾すべきです.

*3:この本では ADL は Koenig の自動照合と呼ばれています