#3(の2)可能ならいつでも const を使う.

Effective C++ 3項の内容です.

const なメンバ関数とは??

前回のエントリで変数に対する const の説明をしました.今回は,関数に対して使う const の説明です.クラスのメンバ関数に対して const をつけることによって,そのメソッドが当該オブジェクトに対して何も変更をもたらさないことを宣言できます.サンプルコード全体は最後でのせるとして,細かな説明をしてみます.まず,ConstSampleのメンバ関数として,下記のconstFunctionを宣言しました.このメンバ関数の後ろに const がついていますが,これの意味するところは,「この関数はこのオブジェクトの状態を何も変更しません!」ということです.もしこの関数がオブジェクトの状態を変更してしまおうとしても,コンパイルが通りません.

  void constFunction() const;

ということで,いくつか実験してみました.

1.クラスのメンバ変数を思いっきりダイレクトに代入してみる.
"error: assignment of member ‘ConstSample::m_x’ in read-only object" というエラーメッセージでコンパイルが失敗します.

void ConstSample::constFunction() const {
  m_x = 10;
}

2.クラスのメンバオブジェクトのメソッドをそれとなく呼んでみる.
ConstSampleには,vectorをメンバとしてもたせました.vector::clearやpush_backは明らかにvectorの状態を変えてしまいますが,これもきちんとコンパイルエラーになります.vectorメンバ関数として定義されている clear や push_back はそれ自身が const として宣言されていません.このような関数は文字通り vector の状態をかえてしまう可能性があります.コンパイラはこの情報を手がかりに,この関数を呼び出すと状態を変更してしまう可能性があることを知り,コンパイルを失敗させます.
"error: passing ‘const std::vector’ as ‘this’ argument discards qualifiers [-fpermissive]"

  vec.clear();
  vec.push_back(10);

3.クラスのメンバ変数(ポインタ)が指す領域の値を変更する.
Effective C++でも,問題として取り上げられていましたが,これはコンパイルエラーにはなりません.あくまでもポインタ変数(=アドレスを持つ変数)がメンバなのであって,ポインタ変数の指す先の値がどうなろうが知ったこっちゃない!ということみたいです.下記,サンプルコードでの実験結果とも一致します.

  m_y[2] = 10;

4.const 定義されていない他のメンバ関数を呼び出す.
これはコンパイルエラーになります.理由としては,const宣言されていない関数を呼び出すとその関数がオブジェクトの状態を変更してしまう可能性があるからです.
"error: passing ‘const ConstSample’ as ‘this’ argument discards qualifiers [-fpermissive]"

  someFunction();

5.const 定義されていない参照変数としてメンバを返すメソッド.
これもコンパイルエラーになります.理由としては,参照変数を返した先の未知世界で参照変数が更新されるかもしれない(=オブジェクトの状態が変わってしまう)からです.
"error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers"

int& ConstSample::getVal() const {
  return m_x;
}

ちなみに,const int& を返す関数として下記のように定義すると,コンパイルが通るようになります.

const int& ConstSample::getVal() const {
  return m_x;
}

こうやって実験してみると,これ便利ですね.確かに,可能ならいつでも const つけると無駄な混乱を避けてとても使いやすくなりそうです.

#include <iostream>
#include <vector>

class ConstSample {
public:
  ConstSample() 
    : m_x(0), vec()
  {
    m_y = new int[10];
  }

  ~ConstSample()
  {
    delete[] m_y;
  }

  void constFunction() const;

  void nonConstFunction();

  void someFunction();

  const int& getVal() const;

private:
  int m_x;
  int *m_y;
  std::vector<int> vec;
};

void ConstSample::constFunction() const {
  // error: assignment of member ‘ConstSample::m_x’ in read-only object
  // m_x = 10;

  // error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
  // vec.clear();

  // error: passing ‘const std::vector<int>’ as ‘this’ argument discards qualifiers [-fpermissive]
  // vec.push_back(10);

  m_y[2] = 10;

  // error: passing ‘const ConstSample’ as ‘this’ argument discards qualifiers [-fpermissive]
  // someFunction();
}

void ConstSample::nonConstFunction() {
  m_x = 10;

  vec.clear();

  vec.push_back(10);

  m_y[2] = 10;
}

void ConstSample::someFunction() {

}

const int& ConstSample::getVal() const {
  return m_x;
}

int main(int, char**) {

  std::cout << "Effective C++ 3項" << std::endl;

  return 0;
}

オーバーロードを用いた const / 非const な関数の呼び分け

C++は同名でシグニチャが異なる複数の関数を宣言できました.これは const / 非 const でも同じように適用できます.つまり,

同名の関数を const / 非 const の違いだけで宣言できる.

で,実際に呼び出されるときは const / 非 const を見て,合致した関数が呼び出されます.下記,サンプルコードです.[]演算子を宣言しましたが,const バージョンと 非 const バージョンの2パターン用意しました.この関数がどのように呼び分けられるかを,下記の2つの関数で試します.

void printSampleVector(const SampleVector& samVec) {
  std::cout << samVec[1] << std::endl;
}

void modifySampleVector(SampleVector& samVec) {
  samVec[1] = 100;
}

実際の実行結果は下記のようになりましたが,const バージョンと 非 const バージョンの operator[] がきちんと呼び分けられている様子がわかりました.

Effective C++ 3項
Calling printSampleVector(const SampleVector&) <- const 参照渡しをしているので,呼び出される operator[] は const バージョン.
Const version of operator[]
1
Calling modifySampleVector(SampleVector&) <- ただの参照渡しをしているので,呼び出される operator[] は 非const バージョン.
Non const version of operator[]
Calling SampleVector::pringAllElems
0
100
2


ということで,2つ目のサンプルコードです.

#include <iostream>
#include <vector>

class SampleVector {
public:
  SampleVector()
  {
    vec.push_back(0);
    vec.push_back(1);
    vec.push_back(2);
  }

  ~SampleVector()
  {}

  int& operator[](int pos) {
    std::cout << "Non const version of operator[]" << std::endl;
    return vec[pos];
  }

  const int& operator[](int pos) const {
    std::cout << "Const version of operator[]" << std::endl;
    return vec[pos];
  }

  void printAllElems() const {
    for (unsigned int i = 0; i < vec.size(); i++) {
      std::cout << vec[i] << std::endl;
    }
  }

private:
  std::vector<int> vec;
};

void printSampleVector(const SampleVector& samVec) {
  std::cout << samVec[1] << std::endl;
}

void modifySampleVector(SampleVector& samVec) {
  samVec[1] = 100;
}

int main(int, char**) {

  std::cout << "Effective C++ 3項" << std::endl;

  SampleVector samVec;

  std::cout << "Calling printSampleVector(const SampleVector&)" << std::endl;
  printSampleVector(samVec);

  std::cout << "Calling modifySampleVector(SampleVector&)" << std::endl;
  modifySampleVector(samVec);

  std::cout << "Calling SampleVector::pringAllElems" << std::endl;
  samVec.printAllElems();

  return 0;
}