クラスに持たせる定数について

動機: 定数の定義 - C++でゲームプログラミング


上記の記事のように、あるクラスの持つ何らかの特性をコンパイル時に得たい場合、
普通に static メンバ変数を使って

struct Vector3D
{
  // const は constexpr の方がより望ましい
  static std::size_t const dimension = 3;
  
  typedef float real_type;
  real_type x, y, z;
  
  Vector3D()
    : x(), y(), z() {}
  
  Vector3D( real_type x_, real_type y_, real_type z_ )
    : x(x_), y(y_), z(z_) {}
};

とやる場合、あるいは C++0x のように static メンバ関数を使って

struct Vector3D
{
  // constexpr はあれば
  static /*constexpr*/ std::size_t dimension() {
    return 3;
  }
  
  // 以下略

};

とやる場合、あるいは上記の記事の後半のように 整数定数クラスを使って

#include <boost/type_traits/integral_constant.hpp>

template<std::size_t N>
struct dimension
  : boost::integral_constant<std::size_t, N> {};

struct Vector3D
{
  typedef dimension<3> dimension_type;
  
  // 以下略

};

とやる場合、もしくは C++0x の std::tuple のように外部特性クラスを使って

#include <boost/type_traits/integral_constant.hpp>

template<class T>
struct dimension {};

struct Vector3D
{
  // 略

};

template<>
struct dimension<Vector3D>
  : boost::integral_constant<std::size_t, 3> {};

とやる場合など、いろいろと方法があるわけですが、ではどれを採用すればいいか、という事に関し、それぞれの実装の持つメリットを考えてみます。


まず上の二つのように、静的メンバ変数/関数として持たせる場合のメリットは、単純で分かりやすい点。
特にインスタンス経由でアクセスさせる場合には、

Vector3D vec;

assert( vec.dimension == 3 ); // あるいは vec.dimension() == 3

のように、割と直感的にアクセスすることができます。
プログラムにおいて、読みやすさ、というのは大事である以上、これは大きなメリットです。
また、余計なことをしないシンプルな実装方法であり、ソースコードの量が少ない、というのも素晴らしい。


では変数と関数のどちらが良いかですが、これは

  • 変数の場合は C++0x を待たずにコンパイル時定数として扱える。ただし整数型の場合のみ
    • 関数の場合は constexpr を付けないとコンパイル時定数にはならない
    • 整数型でなければどちらにせよ constexpr が必要になる
  • 関数の場合は変数の実体が原理的に発生しない
    • ただし関数の実体は生成されるかもしれない
    • 普通は &Vector3D::dimension なんて書かないので特に気にする必要はないかも
  • 関数の場合は () が余計に必要になる
    • 一方で () がないと落ち着かない人もいる
    • 動的に次元が決定する場合には一般に関数呼び出しになるので、その時との兼ね合い

といった点を踏まえて決めればいいかと思います。

特に大事なのは、 C++98/03 の範囲では、関数にするとコンパイル時定数でなくなってしまう、という点と、
一方で変数にすると、実行時に次元が変化する場合に上手く対処できない、という点です。
ここから考えて、一般には、動的に次元が変化することがあり得る場合には関数に、
そうじゃない場合には変数にするのが良いデザインでしょう。


次に型特性として扱う場合は、上記のようなインスタンス経由でのアクセスは行えないので、
基本的にはメタプログラミング時に、その情報を使うことになるでしょう。

こうすることによる利点は、まず SFINAE *1 を行い易い、というのがあります:

template<class T>
inline typename T::dimension_type::value_type get_dimension( T const& )
{
  return T::dimension_type::value;
}

上記のコードは、 T に dimension_type があればその value を返すという関数です。
この関数は SFINAE によって「 dimension_type が無ければ、そもそも実体化されない*2」ため、
例えば他の get_dimension を定義して、 dimension_type が無ければそっちを呼ばせる、といったことが可能になります。
これを静的メンバ変数/関数を使って行おうと思うと、 C++98/03 の範囲では多分無理ですし、 C++0x なら decltype を使って可能ですが、その場合は少しばかりトリッキーなコードになり、可読性が若干落ちます。
また他の利点としては、クラスなので Boost.MPL のメタ関数に直接渡すことができ、これは極めて大きな利点です。
またちょっとした利点としては、 integral_constant をタグとして tag dispatch*3 を行いやすい、という点もあり、これは覚えておくと便利です。
いずれにせよ、このように特性を持たせたクラスを使う場合、一見して奇妙に見えますが、
メタプログラミングなどの比較的トリッキーなことをしたい場合には、余計な手間を省けて便利なのです。


で、内部に typedef として持たせるか、外部のメタ関数を特殊化するか、どちらが良いかですが、

  • 内部にメンバとして持たせた場合、後になって特性を外から追加することが出来ない
    • どうしても外から追加させたい場合には、結局、何らかのメタ関数が必要になってしまう
      • その場合、今まで X::xxx_type って書いてきたところを、全部書き換える必要あり
    • 既存のクラスを扱いたい場合、ラッパークラスを書くなりしないと扱えない
      • ラッパークラスを使わず直接扱いたい場合は、クラスの定義そのものに変更を加える必要がある
  • 外部に特性メタ関数を作った場合、 const や volatile といった修飾子に影響を受ける
    • ちなみに X::xxx_type の場合でも、参照に対しては対処できない(( X が T& のとき、 X::xxx_type という表記は ill-formed 。 C++0x の Perfect Forward を使っていると引っかかるので注意( template typename T::value_type get_value( T && x ); という関数に lvalue を渡すと T は参照型になり、 T::value_type は定義されてないため SFINAE によって検索候補から外され、”get_valueが見つからなかった”的なエラーになる)))
    • とはいえ、実は、これに関しては対応可能。下記。
  • 外部に特性メタ関数を作った場合には、継承した場合には特性メタ関数を派生クラスに対しても定義しないといけない
    • 内部に typedef した場合、通常の継承と同じように継承される
      • とはいえ、時として勝手に継承されるとまずいこともあるので、これは一長一短

といった兼ね合いから決めていくことになります。

この中で、「 const や volatile (それから参照)に弱い」という弱点は結構大きいですが、これは

template<class T>
struct dimension {};

template<class T>
struct dimension<T const>
  : dimension<T> {};

template<class T>
struct dimension<T volatile>
  : dimension<T> {};

template<class T>
struct dimension<T const volatile>
  : dimension<T> {};

// 参照対応も必要なら
template<class T>
struct dimension<T&>
  : dimension<T> {};

// C++0x はこれに加えて && も


// こうしておけば、各々クラスに対する特殊化は一つで良い
template<>
struct dimension<Vector3D>
  : std::integral_constant<std::size_t, 3> {};

のように部分特殊化を行うことによって、容易に対処できます。
むしろこのようにすれば、 typedef では明示的に remove_reference する必要がある場合にも問題なく扱えるので、この点に関してはむしろ利点である、とすら言えます。
なので、継承時に特性も継承させられるメリットと、外部から非侵入的に特性を追加できるメリット、この二つの兼ね合いで、どちらを使うかを決めていけばいいでしょう。


で、結局、どれを採用すればいいか、ですが、これはハッキリ言って用途によると思います。
ちなみに僕個人としては、関数を使って Vector3D::dimension() のようにするのが好みで、
なぜならば、次元情報を元にメタプログラミングを行うようなことは 通常 考えにくいですし、
そういう状況よりは、実行時に次元が変わるようなデータ構造、という方が多いと考えられるからです。
とはいえ、コンパイル時に次元を取得できないのはかなり痛いので、
C++0x で constexpr が実装されるまでは*4、静的メンバ関数や特性メタ関数の使用も考慮して、というのが現実的ではないでしょうか。


* * *


なお、ここからは余談になりますが、このように、「クラスに対し何か定数を持たせたい」といった単純な問題でも、様々な実装方法があり、それぞれにメリットとデメリットがあるのが、 C++ という言語です。
それが良いか悪いかの議論は置いておくとして、そういう言語を使う以上、
自分が何故そのような実装方法をとるのか、別に良い方法はないのか、
ということは、常に念頭に置くべきでしょう。
勿論、プログラムを作る上で、それだけの余力がないことも多いでしょうから、
そういう場合には、自分のよく知る方法を使うようにして、
「メリットは分からないけど〜〜がこうしてるから」
といった半端な理由によって、慣れないデザインを採用することは、避けたほうが賢明なのかな、と思う次第です。

*1:簡単に言うと、関連する型が必要な条件を満たしていない場合でも、そこで直ぐにエラーにせず、別の関数に切り分けたりする、といったことが出来るようになる仕組み。詳しくは検索してください

*2:つまり別の関数を探しに行く

*3: std::distance 等の実装に使われてるテクニックで、クラスの「タグ」を関数の引数に渡すことで、柔軟に実装を切り替えるテクニック。詳しくは検索を。

*4:っていうか開発中の gcc 4.6.0 では既に実装済