to const or not to const
C++ のconst correctness
についてmutable
の説明する時に少しだけ触れたことがありました
最近あるクラスのゲッターにconst
を付けるだけのリファクタリングをしたので、もうちょっとconst
について詳しく説明したいと思います。
Rust のような変数や引数が全部デフォルトで変更不可能になっているプログラミング言語があれば、Python のような定数という概念が元から無い言語もあります。
C++ にはconst
があって、変数や引数、関数にも付けられるようになっています。でも to const or not to const という冗談があるぐらい、どれぐらい付ければいいのかについては、C++ のプログラマーの意見が分かれています。
個人的にはconst
を使うのはコードが読みやすくなる時だけです。話題別に列挙すると以下のようになっています。
メンバ関数
絶対に付けて欲しいです。
現に今回あるクラスのゲッターにconst
を付けるだけの Diff を作ったのも、以下のケースがあったからです。
int getA();
int getB(); // ここだけメンバー変数を変更している
int getC();
ゲッターは値を返すだけなはずなのに、まれに何らかの事情でメンバー変数を変えるゲッターもあります。もちろんそこは絶対にリファクタした方がいいのですが、そこは置いといて、以下のようにconst
を付けた方が、このゲッターは何か違うことをやっているのですねってヘッダーを見るだけで一瞬で分かります。
int getA() const;
int getB();
int getC() const;
メンバ変数
付けない方がいいです。
クラスや構造体の変数にconst
を付けると、そのクラスや構造体がムーブされる時に、const
が付いているメンバ変数がムーブではなくコピーされるようになってしまいます。
これは以下のコードで確認できます。
#include <iostream>
#include <string>
struct MyMember {
MyMember(std::string n) : name(n) {}; // コンストラクタ
MyMember(const MyMember& other) : name(other.name) { std::cout << name << ": copy ctr\n"; } // コピーコンストラクタ
MyMember(MyMember&& other) : name(std::move(other.name)) { std::cout << name << ": move ctr\n"; } // ムーブコンストラクタ
std::string name;
};
class MyClass {
public:
MyClass(MyMember mem1, MyMember mem2)
: m_mem1(mem1.name) // 普通のコンストラクタが発動するようにわざと std::string として渡す
, m_mem2(mem2.name)
{}
// デフォルトのコピーコンストラクタを無効する
MyClass(const MyClass&) = delete;
MyClass(MyClass&) = delete;
// デフォルトのムーブコンストラクタだけ有効にする
MyClass(MyClass&&) = default;
private:
const MyMember m_mem1;
MyMember m_mem2;
};
int main() {
MyClass test{{"num1"}, {"num2"}};
MyClass otherTest = std::move(test);
}
以下の出力になります。
num1: copy ctr
num2: move ctr
メンバ変数はconst
を付けずに、ゲッターやセッターだけでアクセスするようにして、外から変えられないようにすればいいです。
参照型とポインタ型の引数
絶対に付けて欲しいです。値渡しの引数は関数内で変更しても最終的に意味がないので、付けても付けなくてもいいです。
int setData1(const float* A, int* B);
int setData2(const std::vector& A, std::list& B);
これだとヘッダーを見ただけで、どれが入力引数なのか、どれが出力変数なのか一瞬で分かります。
ローカルの変数
付けて欲しいですが、コードが少し読みやすくなるだけなので、好みになります。
const std::string filename = param.getFileName();
std::string path_to_file = param.createPath();
これだったら、filename
はもう変わらないですが、path_to_file
は後で変わるかもしれないため、path_to_file
の方にもっと注意することが出来ます。
まとめ
やはり、優先度的にメンバ関数と参照型とポインタ型の引数には絶対に付けて欲しいです。
なぜかというと、1つの関数にconst
を付けないだけで、コールする関数すべてにconst
を付けられなくなります。
参照型とポインタ型の引数も同じで、引数がconst
になっていないと、その関数をコールする時にconst
の変数を指定できなくなります。
他のコードにも影響があるため、そこは個人的な判断では駄目だと思います。
メンバ変数の場合は、付けると、その変数がムーブされなくなるため、お勧めはしないですが、パフォーマンスの問題なので、優先度的に低いです。
ローカルの変数は付けた方がコードが読みやすくなりますが、他のコードへの影響が少ないため、任意だと思います。
Discussion
概ね間違っているとまでは思わないのですが、状況次第では気になる部分もあります。
constを付ける/付けないは、実用上の理由もありますが、意図の明確化という意味合いも強いと思います。
constを付けたメンバ関数内は*thisがconstになります。実装当初にたまたまconstで良かったメンバ関数にconstを付けてしまうと、何かの変更があったときにconstに出来ないというケースが稀にあるので、機械的に付けるのではなく、意味合い的にconstになるべきなのかを考えた方がいいと思います。この部分の設計を間違えると、修正が大幅な手間になります。
メンバ変数には付けない方がいい、とかそういう話も問題です。インスタンス生成時に付けたら以降破棄されるまで変わらないべきならconstにすべきです。C++のような言語が扱うドメインでは往々にしてコピーされること自体が好ましくないものも多いと思いますよ。
またパフォーマンスという意味では、constexprも考慮すべきなので、これについても触れるべきです。
なので、個人的には
までにして、こうすべき、というのは言わなくていいと思いますよ。こういうのは機械的に出来る話までにすべきかと…