【C++】const の扱いが複雑すぎる その2- 言語仕様で決まっていない運用面の問題について

C++ の const の基本的な使い方は以下の記事にまとめています。

今回は言語仕様で明確に決まっていないところ(運用面の問題)について説明します。

もくじ

関数の引数

関数の引数に const を付ける場合、何を基準にすれば良いのか?

大前提
  1. 引数で値を受け取る場合はつけない
  2. 引数で値を受け取らない場合は状況による

引数で値を受け取る場合はつけない

関数が計算結果を返すが、計算した結果、異常な値になってしまったときに計算結果とは別にエラーを返したいようなケース。

int f(int in_arg, int& out_error) {
  int result = in_arg /* を使って色々計算する */;
  if (result == 異常値) { out_error = -1; }
  else { out_error = 0; }
  return result;
}

このようなケースでは、const を付けると out_error の値を変更できなくなってしまうので、const は付けません。

関数内で構造体やクラスのインスタンスのメンバ変数を変更して返すようなケース。

struct Hoge {
  int moge;
  ... 略
};
int GetHoge(Hoge& out_hoge) {
  hoge.moge = /* 値を設定して返す */;
  if (問題発生) { return -1; } // エラー値を返す
  略
  return 0; // 問題は起きなかった
}

out_hoge は受け取り用なので const を付けるとメンバ変数を変更できなくなってしまいます。
この場合も同様に付けません。

引数で値を受け取らない場合は状況による

プリミティブな型には付ける?付けない?
void Hoge(const int in_arg) { /* 略 */ }

議論が分かれるのは、int/char/long long/float などの基本の型(プリミティブな型)を入力用として引数にした場合 const を付けるべきか?

It’s about self-documenting your code and your assumptions.

If your code has many people working on it and your functions are non-trivial then you should mark const any and everything that you can. When writing industrial-strength code, you should always assume that your coworkers are psychopaths trying to get you any way they can (especially since it’s often yourself in the future).

Besides, as somebody mentioned earlier, it might help the compiler optimize things a bit (though it’s a long shot).

以下は↑の英文を翻訳ツールで日本語翻訳した後、若干私が手直ししたものです。
DeepL 翻訳を使用しました。

const はあなたのコードとあなたの仮定を自己文書化することです。
もしあなたのコードに多くの人が関わっていて、関数が自明でないなら、あなたはできる限りあらゆるものを const でマークする必要があります。商業用の堅牢なコードを書くときは、同僚がどんな方法でもあなたを捕まえようとするサイコパスであることを常に想定すべきです(特に、それが将来的に自分自身になることが多いので)。それに、先ほど誰かが言っていたように、コンパイラが少しは最適化してくれるかもしれません(望みは薄いですけど)。

こちらのコメントをされた方とは良い酒が呑めそうです。

私が過去に所属していたいくつかのチームでも意見が割れてました。

「引数が const だから、いちいち cast が必要になって面倒。」
「引数の const は実装者側の都合で付けてるものなんだから、使う側に強要しないで欲しい。」

このような意見もありました。

私にとってはコードのあらゆる面で実装者の意図が明確になっている必要があると考えているので(とても難しいことですが)、引数の値が関数内で変わらないことを明示することには、説明がへたくそなプログラマが書いたコメントより、はるかに意味があると思います。
コメントを書かなくても理解できるコードになっていることの方が重要です(コメントを書くなという意味ではないです)。

Unreal Engine のコーディング規約でも const を付けるように定められています。

「Const を正しく設定する」に記載されています。

Google C++ スタイルガイドでも推奨されています。

日本語訳したものですが、マウスオーバーすることで原文も確認できます。

扱うプラットフォームによって clang だったり gcc だったりコンパイラの種類もバージョンも様々ですが、const を付けても付けなくても動作は変わりません。
とは言え、いつも例外は起きるので、const のありなしで動作が変わってしまう場合や、コード解析プログラムなどがうるさい場合は、そちらに合わせて変えれば良いと思います。

void Hoge(const int moge) {}

int uge { -1 };
Hoge(const_cast<const int>(uge)); // このキャストは基本的に必要ない
Hoge(uge); // キャストしなくても動作は変わらない

基本の型については、以上のように至極曖昧です。

サイズが小さいものには付ける?付けない?

分かりにくいのが、以下のようなケース

// この構造体のサイズは 32bit
struct Hoge {
  int32 moge;
};

// この配列のサイズは 64bit
Hoge hoges[] = { new Hoge(), new Hoge() };

// enum のサイズは int と同じ(環境で変わるかも?)
enum Uge {
  Uge_Value1,
  Uge_Value2,
  ...
};

// これは 8bit
enum class Bubera : std::int8_t {
};

// これは 128bit
enum class Hebushi : unsigned __int128 {
  Value1 = 0x12345678123456781234567812345678, // 128bit値
};

上記のようなケースも、基本の型とサイズが変わらないので、型ではなく、型のサイズで決めるケースもあります。
enum も C++11 で型を指定できるようになったので、その型のサイズ次第で const をつけるべきか判断が必要になってくると思います。

struct や class のインスタンスには付ける?付けない?

クラスや構造体の場合、実装したときはサイズが小さくても、どんどんメンバが追加されていって大きくなる可能性があるので、const を付けて参照を渡す形にするのが無難です。

struct Hoge {
  int moge;
  char uge[10];
};

// struct か class で入力用の引数なら const を付けておくのが無難
void Bubera(const Hoge& in_hoge) {}

const を付けて参照を渡すべきかどうかの判断が必要になる理由は、サイズが大きいとコピーするのに時間がかかるからです。
コピーする必要があるなら問題ありませんが(コピーしないとならない状況が思い当たらないですが…)、そうでないなら、CPU 時間もメモリも無駄です。
OS が 64bit なら 64bit を超える場合に const を付けて参照にするのが無難ですが、CPU が優秀だと 128bit のコピーも速かったりしますので、環境によって変わります。

ポインタを適切に扱わないプログラムはすぐ死ぬ

ちょっと横道にそれますが、ポインタと参照について少し説明します。

参照を使えるなら参照を使います。
参照にするか、ポインタにするかの判断は、nullptr を指定する必要があるかどうかで決めます。

nullptr 指定
必要 ⇒ ポインタ
不要 ⇒ 参照

ポインタを扱う場合は生ポインタを避ける必要があります。
boost::shared_ptr などのスマートポインタを使うか、std::unique_ptr などのポインタ用のテンプレートクラスを使います。
生ポインタは、どうしても必要なときだけ、かつ、限られたスコープの中でのみ使います。

class Hoge { 略 };

void Moge(const boost::weak_ptr<Hoge>& hoge) {
  std::cout << *hoge.lock() << std::endl;
}

int main() {
  boost::shared_ptr<Hoge> hoge { new Hoge() };
  Moge(hoge);
  return 0;
}

スマートポインタが開発された経緯をAIに聞くか、検索するか、コンピューター科学の論文を漁るか、大型書店や図書館で書籍を探すと良いです。
その話はとても長くなるので割愛します。

廃版になっているので中古でしか購入できませんが、私は以下の書籍で知り、そのあと、色々な形で情報を得て、自分でもスマートポインタクラスを作ってみたりなどして知見を得ました。
1だったか、2だったかは思い出せません。
もう所有していないので確認もできず…。

このあたりにも書かれていたような…。

関数の戻り値

関数の戻り値は基本的に const は付けません。
付けるとめんどくさいからです。
めんどくさい上に、安全性が向上したり、開発効率が上がったりしないので、無駄な手間になります。

const int Plus(int a, int b) { return a + b; }

const int plused_value { Plus(1, 1) };
plused_value += 1; // エラー

// めんどくさい
int plused_value { static_cast<int>(Plus(1, 1)) };

// const_cast の const 外しは未保障
int plused_value { const_cast<int>(Plus(1, 1)) };

// めんどくさい
const int const_plused_value { Plus(1, 1) };
int plused_value { static_cast<int>(const_plused_value) };
struct Hoge {
  略
};

const Hoge& GetHoge() { 略 }

Hoge hoge { GetHoge() }; // 暗黙の型変換 これ安全?

const Hoge& hoge { GetHoge() }; // hoge の寿命はいつ終わる?

以下のような運用は普通にあります。

class Moge {};

class Hoge {
  boost::shared_ptr<Moge> m_moge { new Moge() }; // インスタンスが入ってる
  boost::weak_ptr<Moge> m_weak_moge {}; // m_moge の参照用
public:
  Hoge() : m_weak_moge(m_moge) {}
  const boost::weak_ptr<Moge>& GetMoge() const { return m_moge; }
};

// hoge はどこかで生成したもの
if (const auto& moge { hoge.GetMoge().lock() }) {
  moge->なんかのメンバ;
}
struct Hoge {
  略
};

class Moge {
  Hoge m_hoge {}; // スタック管理
public:
  // m_hoge のコピーを返す
  Hoge CopyHoge() const { return m_hoge; }
};

Hoge copied_hoge { moge.CopyHoge() };

コピーすることを意図しているなら、戻り値でインスタンスを返すのはありです。
返したインスタンスを「絶対に」変更して欲しくないなら、const を付けるのもありです。

class Moge {
  Hoge m_hoge {}; // スタック管理
public:
  // 戻り値の Hoge は参照専用として使われることを想定しているため
  // 受け取り側で値を変更しないで下さい。
  // 主に m_hoge の値をログに表示するなどして意図した通りの値に
  // なっているのかを確認するために作った関数です。
  // m_hoge の値を変更したい場合は専用のメンバ関数を別途
  // 作成してください。
  const Hoge GetCopiedConstHoge() const { return m_hoge; }
};

const Hoge copied_const_hoge { moge.GetCopiedConstHoge() };

なんとなくで const を付けるのはナシです。
const を付けるにしても、付けないにしても、明確な意図をもって決める必要があります。

また、明確な意図をもって決めたのなら、それをコメントで説明する必要があります。
コードから処理を読み取ることはできますが、作成した人の意図を読み取ることはできません(一部の優秀な人はできたりしますが…)。

ローカル変数

デフォルトで const を付けます。
変更することが決まっている場合だけ const を付けないようにします。

const が付くことで、その変数の値が変わらないことが明確になります。
どこで値が変わるのか?を覚えておく必要がなくなり、何らかのバグの調査をしているときに、どこで値が変わっているのかを追跡する手間を省くことができます。
些細なことですが、実装の4倍のコストがかかる保守において効果は大きいです。

const int a { 10 };
int b { a + 2 };
std::cout << b++;
// ポインタは2つ付ける必要あり
const char* const a { "hoge" };

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です