syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

shared_ptrとconst

(これは2011-01-17に書いた故OCNブログの記事を加筆修正したものです)

あんまり言及してる人がいないんですが、これはshared_ptrとconstを組み合わせて使う上でとても重要な機能です。

普通組込のconstポインタあるいはconst参照経由では、const操作しかできません。

例えば、

class Vector3F
{
public:
  float x, y, z;
public:
  Vector3F() : x(), y(), z() {}
  Vector3F(float inX, float inY, float inZ) : x(inX), y(inY), z(inZ) {}
public:
  // ベクトルの長さを返す。※内部状態を変更しない。
  float GetLength() const
  { return sqrt(x * x + y * y + z * z); }
  // 自身を正規化する。※内部状態を変更する。
  void Normalize()
  {
    const float len = this->GetLength();
    if (len > 0)
    {
      this->x /= len;
      this->y /= len;
      this->z /= len;
    }
  }
};

なクラスがあったとして、

Vector3F vec(1, 2, 3);
const Vector3F* pReadonlyVector = &vec;
float len1 = pReadonlyVector->GetLength();
const Vector3F& rReadonlyVector = vec;
float len2 = rReadonlyVector.GetLength();

はOKだけど、

pReadonlyVector->Normalize();
rReadonlyVector.Normalize();

コンパイルエラーになります(今回、変数名のpプレフィックスやrプレフィックスは便宜上付けているだけですのであしからず)。

こういう「内部状態を変更しない」ことをコンパイル段階で表明し、堅実なプログラムを記述するのに非常に役立つのがC++のconst修飾子(const modifier)であり、C#Javaにはない優れた機能だと思います*1
C++プログラマーのスキル レベルやライブラリの品質を評価する場合、このconstをきちんと理解して使っているかどうかが一つの指標になります。というかconstをきっちり使っていないC/C++ライブラリなんて怖くて使う気になれません。

ここで、型Tのconst組込ポインタ型に対応するスマートポインタとして、Boostライブラリのboost::shared_ptr、C++0x TR1のstd::tr1::shared_ptr、もしくはC++11のstd::shared_ptrでconstスマートポインタ型を作るのであれば、

typedef shared_ptr<const T> TMyConstSharedPtr;

あるいは

typedef shared_ptr<T const> TMyConstSharedPtr;

とします。このconstスマートポインタ型からはconst操作しかできなくなります。

なお、constでないスマートポインタ型はconstスマートポインタ型に暗黙的な代入が可能となっています。これはちょうど下記のように、constでない組込ポインタ型がconst組込ポインタ型へ暗黙的な代入が可能になっているのと同じです。

Vector3F vec(1, 2, 3);
Vector3F* pVector = &vec;
pVector->Normalize();
const Vector3F* pReadonlyVector = pVector;

こういった通常のプリミティブ型に用意されている機能をそのまま使えるように模倣してくれているところがC++テンプレート ライブラリの醍醐味と言えます。

なお、const shared_ptr<T> と shared_ptr<const T> の違いは下記を見れば一目瞭然です。

#include <iostream>
#include <memory>
#include <vector>
#include <cassert>

typedef std::vector<int> TIntArray;
typedef std::shared_ptr<TIntArray> TIntArraySharedPtr;
typedef std::shared_ptr<const TIntArray> TIntArrayConstSharedPtr;

void Print(const TIntArrayConstSharedPtr& inArray)
{
  assert(inArray);
  for (auto x : *inArray)
  {
    std::cout << x << std::endl;
  }
  //inArray->clear(); // Compile error will occur
}

void Multiply(const TIntArraySharedPtr& inoutArray, int opMul)
{
  assert(inoutArray);
  for (auto& x : *inoutArray)
  {
    x *= opMul;
  }
  //inoutArray->clear(); // No comile error
  //inoutArray = std::make_shared<TIntArray>(); // Compile error will occur
}

int main()
{
  TIntArraySharedPtr myArray = std::make_shared<TIntArray>();
  for (int i = 0; i < 10; ++i)
  {
    myArray->push_back(i);
  }
  Multiply(myArray, 2);
  Print(myArray);
  return 0;
}

shared_ptrとキャスト

ちなみに派生クラスのshared_ptrは基底クラスのshared_ptrにそのまま代入(アップキャスト)することができますが、基底クラスのshared_ptrから派生クラスのshared_ptrへダウンキャストしたり、基底クラスAのshared_ptrから基底クラスBのshared_ptrへクロスキャストしたりする場合(組込ポインタであればstatic_castやdynamic_castを使う場面)は、static_pointer_cast()関数やdynamic_pointer_cast()関数を使用します。

#include <cstdio>
#include <memory>

class BaseA
{
public:
  virtual ~BaseA() {}
};

class BaseB
{
public:
  virtual ~BaseB() {}
};

class Derived : public BaseA, public BaseB
{
public:
  Derived() {}
};

int main()
{
  using std::shared_ptr;

  shared_ptr<Derived> pDerived1 = std::make_shared<Derived>();
  printf("pDerived1 = 0x%p\n", pDerived1.get());
  shared_ptr<BaseA> pBaseA = pDerived1; // アップキャスト。
  printf("pBaseA = 0x%p\n", pBaseA.get());
  shared_ptr<Derived> pDerived2 = std::static_pointer_cast<Derived>(pBaseA); // ダウンキャスト。
  printf("pDerived2 = 0x%p\n", pDerived2.get());
  shared_ptr<BaseB> pBaseB = std::dynamic_pointer_cast<BaseB>(pBaseA); // クロスキャスト。
  printf("pBaseB = 0x%p\n", pBaseB.get());

  // 継承関係が明らかな場合は static_pointer_cast を使える。
  // 実行時に継承関係をチェックする必要がある場合、dynamic_pointer_cast を使う。

  return 0;
}

ちなみにconst_castへのアナロジーとして、同様にconst_pointer_cast()関数も存在しますが、一応用意されているというだけなので使う機会はまずないでしょう(そう願いたい)。

shared_ptrとインテリセンス

VC++ 2008 SP1にはstd::tr1::shared_ptr(<memory>ヘッダーをインクルードすることで使用可能)が実装されていて、これはboost::shared_ptr(<boost/shared_ptr.hpp>ヘッダーをインクルードすることで使用可能)とほぼ同じものなんですが、どちらもヘッダーをインクルードした途端なぜかインテリセンス(コード補完機能)が効かなくなることがあります。こういうとき、「追加のインクルード ディレクトリ」に「./」もしくは「.\」(カレント ディレクトリの相対パス)を追加しておくと、インテリセンスがちゃんと効くようになることがあります。詳しいことはよく分かっていません。VC++ 2012などではstd::shared_ptrを使うときもちゃんと普通にインテリセンスが機能するので、一応この問題は修正されているように思います。

余談ですが、こういうディレクトリ パスやファイル パスをプロジェクト プロパティで設定するとき、スラッシュ(0x2F)とバックスラッシュ(0x5c)どちらを使ってますか? 自分はVisual C++環境変数($(SolutionDir)など)を組み合わせるときはバックスラッシュを使って、そうでないときは(UNIX環境と互換性があり、さらに日本語Windows環境で語境界が分かりやすい)スラッシュを使うようにしています。日本語環境ではフォントによってはバックスラッシュが円記号になるので、かなり見にくく(醜く)なります。UNIX標準はパス区切りがスラッシュなんですが、Windowsでは基本的にパス区切りがバックスラッシュではあるものの、アプリケーションやAPIによってはスラッシュもバックスラッシュも両方使える場面があります。あと、自分はC/C++の#includeディレクティブに指定するパス名には、必ず区切り文字にスラッシュを使うようにしていますが、Visual C++のリソース スクリプト(*.rc)では標準でバックスラッシュが使われてたりするので、リソース スクリプトを直接編集する場合はバックスラッシュに合わせています。区切り文字にスラッシュもバックスラッシュも許可する仕様にしてしまったWindowsなんですが、こういうあいまいな仕様はやめて欲しかったです。あとASCIIバックスラッシュをフォントによって円記号に割り当てることを最初に考えた人は本当におバカだなと思いました。

ちなみにシェルのPath系APIや、そのラッパーであるATL::CPathのメソッドには、入力文字列中にスラッシュとバックスラッシュを混ぜるとおかしな結果になるものもあるので、Windows環境ではできるかぎりバックスラッシュで統一することをお勧めします。

*1:C#にはconstとreadonly、Javaにはfinalがありますが、C#のconstはコンパイル時の完全定数のみ、readonlyはコンストラクタのみ、Javaのfinalは参照型の内部状態変更には関与しないなど、C++のconstほどの強力さはありません。