すごいconstたのしく使おう!
const usage guide for C++ beginners
@Regenschauer490
1
はじめに
自己紹介
 .sigure// (@Regenschauer490)
 関西の大学院生
 C++11歴8ヶ月の素人
 const教穏健派
内容:
 const の使い方を中心に、私が経験的に気づいたことを
コード例と一緒に説明していきます。
 右下に「ここまでする必要があるのか?」を表すレベルを
私個人で恣意的につけてみました。
※ Lv 0 は C++を使う上で是非とも守るべきだと思うものです
2
const 概要
cv修飾子の一種
 const 修飾子 : 変数の値を変更できないよう指示
 volatile 修飾子 : 最適化せずメモリへアクセスするよう指示
変数に対する修飾とメンバ関数に対する修飾がある
const 修飾された変数の宣言時には初期化が必要
変数に対する修飾子の位置は 「const T」 「T const」
どちらでもOK
3
定数
定数は, #define の代わりに constを使う
コンパイル時定数になるのは整数型だけ
その他は実行時定数となる
正直、定数は constexpr を使いましょう
4
const int N = 5; //コンパイル時定数
int ar1[N] = { 1, 2, 3, 4, 5 }; // Nは無くても可
array<int, N> ar2{ { 1, 2, 3, 4, 5 } }; // initializer-list
定数
ユーザ定義型(クラス)のconst変数の宣言
 暗黙のデフォルトコンストラクタだけではコンパイルエラー
自分で定義 or default指定 する必要がある
5
struct Hoge1{
string str_;
};
struct Hoge2{
string str_;
Hoge2() = default;
};
Hoge1 hoge1; // OK
Hoge1 const choge1; // error
Hoge2 hoge2; // OK
Hoge2 const choge2; // OK
関数の引数
T const& で引数をとる
 引数元の内容が変更されないことを保証
 Tがユーザ定義型(クラス)なら、値渡しより効率的
 戻り値でローカル変数の参照(or ポインタ)を返すのはNG!
 破棄されたローカル変数へアクセスしてしまう
 moveで返そう
 コンパイラが最適化してくれることもある(RVO/NRVO)
6
string AddGrass(string const& src){
auto tmp = src; // copy
tmp.append(“w”); //appendは非constな操作
return tmp; // move(tmp)
}
関数の引数
引数がプリミティブ型かユーザ定義型か分からない時
 関数の引数がtemplate型の場合どうする?
 boost::call_traits<T>::param_typeで最適な形へ変換
 boost::call_traits<int>::param_type // int const
 boost::call_traits<string>::param_type // string const&
 ただし、関数templateには型推論の問題で使いづらい(下例)
 クラスtemplateに対しては問題なく使える
7
template<typename T>
void Foo(typename boost::call_traits<T>::param_type value){}
Foo(1); // error : Tの型を推論できない
Foo<int>(1); // OK
メンバ関数
クラスの状態を変化させない(メンバ変数を書き換えない)
メンバ関数は、constメンバ関数にしよう
 constメンバ関数内では、メンバ変数は暗黙的にconst修飾される
 const修飾された変数は、 constでないメンバ関数を呼び出せない
 constメンバ関数内では、thisポインタもconst修飾される
8
class Hoge{
string hoge_;
public:
string Get () const{
hoge_ = ""; // error : string const
hoge_.append(""); // error : append() const{…}
SetString(""); // error : this->SetString(){…}
return hoge_;
}
void Set (string const& src) { hoge_ = src; }
};
メンバ関数
constメンバ関数のオーバーロード
 呼び出し元の変数(クラスオブジェクト)が・・・
const変数でない → 非constメンバ関数が呼び出される
const変数である → constメンバ関数が呼び出される
9
class ConstCheck {
public:
ConstCheck (){}
void Check (){ cout << "ないとき!" << endl; }
void Check () const{ cout << "あるとき!" << endl; }
};
ConstCheck ないとき;
ConstCheck const あるとき;
ないとき. Check(); //ないとき!
あるとき. Check(); //あるとき!
メンバ関数
mutable記憶クラス指定子
 constメンバ関数内でも、メンバ変数がconst修飾されなくなる
 主な使い道はクラス内部用キャッシュ
10
class Hoge{
mutable string hoge_; //よくない使い方
mutable vector<chrono::time_point<chrono::system_clock>> time_;
public:
string Get () const{
hoge_ = ""; // OK
hoge_.append(""); // OK
SetString(""); // error : this->SetString(){…}
time_.push_back(chrono::system_clock::now()); // OK
return hoge_;
}
void Set (string const& src) { hoge_ = src; }
};
constの連鎖性11
struct Hoge2{
string str_;
Hoge2() = default;
};
void Foo(Hoge2 const& v){
v.str_.append(“”); // error :v.str _はconst変数
}
 以上から分かるように、constは連鎖する(重要)
 const 変数・メンバ関数からは、アクセスするメンバ変数は
const修飾され、非constメンバ関数は呼び出せない。
 メンバ変数もconst修飾されていれば同様に制限がかかる。
オブジェクトの内側へどんどん伝搬していく
 const_cast や mutable で部分的に破ることはできる
constの連鎖性(代入)12
int a = 0;
int const b = 0;
int& ra = a; // OK
int const& cra = a; // OK
int& rb = b; // error
int const& crb = b; // OK
int d = cra; // copy: T const& -> T
int const& e = []()->int{ return 1; }(); // T(pvalue) -> T const&
 ポインタ・参照への代入時に、元の変数が・・・
const変数でない → ポインタ・参照のconst修飾は任意
const変数である → constポインタ・参照へのみ代入可
 非ポインタ・参照への代入は コピーが行われる
別の変数が新たに作られるので、元の連鎖には関係しない
ラムダ式
コピーキャプチャされた変数は暗黙的にconst修飾
 参照キャプチャされた変数は元のまま
mutableを付けるとコピーキャプチャでもconst修飾されない
13
ConstCheck hoge;
[hoge]{
hoge.Check(); //あるとき!
}();
[hoge]() mutable{
hoge.Check(); //ないとき!
}();
[&hoge]{
hoge.Check(); //ないとき!
}();
ポインタ
C++でポインタといえば shared_ptr
生ポインタ?なにそれ、おいしいの?
 とりあえず、生ポインタのconstからおさらい
14
ptr自体がconstか
ptrが指す先を
const扱いするか
T * const ptr
T & ref
○ ☓
T const * ptr ☓ ○
T const * const ptr
T const & ref
○ ○
※ ついでだから参照も入れときました
ポインタ
生ポインタと同じことがshared_ptrでもいえる
15
 shared_ptr<T> const じゃ中身の値を変更できてしまう!
※私も以前にやらかしてしまいました (^^;)
 typedef shared_ptr<string> StrPtr; とかしてるとやりがち
 typedef shared_ptr<string const> C_StrPtr; も一緒に定義しよう
ptr自体がconstか
ptrが指す先を
const扱いするか
shared_ptr<T> const ptr ○ ☓
shared_ptr<T const> ptr ☓ ○
shared_ptr<T const> const ptr ○ ○
ポインタ
実際にコンパイルして確認
16
ConstCheck a , b;
ConstCheck * const p1 = &a; // p1自体がconst
ConstCheck const * p2 = &b; // p2が指す先をconst扱い
p1 = &b; // error
p1->Check (); //ないとき!
p2 = &a;
p2->Check (); //あるとき!
shared_ptr<ConstCheck> const s1(make_shared<ConstCheck>());
shared_ptr<ConstCheck const> s2(make_shared<ConstCheck>());
s1 = make_shared<ConstCheck>(); // error
s1->Check (); //ないとき!
s2 = make_shared<ConstCheck>();
s2->Check (); //あるとき!
ポインタ
※ ポインタ・参照先が絶対に不変だとは保証しない
 指している先の変数自体は非constかもしれない
 constなポインタ・参照を通しての変更が不可というだけ
17
int a = 0;
int const& cra = a;
a = 1;
cout << cra << endl; // 1
shared_ptr<int> s1(make_shared<int>(0));
shared_ptr<int const> s2(s1);
*s1 = 1;
cout << *s2 << endl; // 1
ポインタ(特にshared_ptr)を使う時は常に意識しておこう
ポインタ
const_cast によるcv修飾子の除去
 constなポインタ・参照から、非constなポインタ・参照を得る
 逆にconst を明示的に付与することもできる
よくある使用例
 古いライブラリの関数へ引数を渡す時
 constメンバ関数の実装を非constメンバ関数で流用する時
 operator[]の実装を、operator[]constと統一したい時など
18
void Print(char* str){ printf("%s", str); }
string s = "example";
string const& crs = s;
string& rs = const_cast<string&>(crs);
rs = "change"; // OK
Print( const_cast<char*>(crs.c_str()) );
ポインタ
ただし、正しく使わないと未定義動作を引き起こす
 const_cast したい constポインタ(参照)の指す先が・・・
const変数でない → cast後に指す先を変更してもOK
const変数である → cast後に指す先を変更すると爆死
 そもそもconst_castを使う時点で設計ミス (とよく言われる)
※自分ではどうしようもない時以外、基本的に使わないでFA
19
int const b = 0; // 元の変数はconst
int const& crb = b;
int& rb = const_cast<int&>(crb);
rb = 1; // Undefined Behavior
ローカル変数
さて、constの重要さを十分理解していただけた所で、
次はローカル変数も可能な限りconstにしていきましょう
まずはあまりイケてないコードを見てみましょう
20
enum class Cookie { CHOCOLATE, GOLDEN, RED };
auto cookie (Cookie::CHOCOLATE);
string v1; //poor
If(cookie = Cookie::RED) v1 = “apocalypsis“; // == のミスでコンパイルエラーにならない
else v1 = "peace";
enum class Cookie { CHOCOLATE, GOLDEN, RED };
auto const cookie(Cookie::CHOCOLATE);
auto const v1 = cookie == Cookie::RED ? "apocalypsis" : "peace";
次にconstの恩恵を享受できるコードを見てみましょう
ローカル変数
他にも同様に見ていきましょう
21
vector<Cookie> cookies{ Cookie::CHOCOLATE, Cookie::GOLDEN, Cookie::RED };
int sum = 0; // poor
for(auto e : cookies){
sum += static_cast<int>(e);
}
vector<Cookie> const cookies{ Cookie::CHOCOLATE, Cookie::GOLDEN, Cookie::RED };
auto const sum = boost::accumulate(cookies, 0,
[](int const sum, Cookie const ck){ return sum + static_cast<int>(ck); }
);
★const意識したらコードがきれいになりました
(23歳 男性 大学院生)
ローカル変数
やむを得ずconstでない変数を作成する場合は、
ラムダ式で非const変数の生存スコープを制限しよう
22
auto const file_pass = “…”; //読み込むファイル
auto const data = [file_pass]{
vector<string> tmp; //後で追加するからconstにできない
string line;
ifstream ifs(file_pass);
while (ifs && getline(ifs, line)) tmp.push_back(line);
return move(tmp);
}();
ローカル関数・スコープを作ることの副産物として、
処理の区切りが自然と生まれ、変更もしやすくなる
ローカル変数23
 ローカル変数をconstにする主なメリット
 変数の使い回しを防ぎ、それぞれの変数の内容・役割を
明確にすることができる.
可読性もアップ↑↑
 ローカル関数・スコープを使うようになる.
処理単位毎にコードがまとまり、変更に強い
 forを使う機会が減り、STLのAlgorithmを使うようになる.
コードが見やすくスッキリ
戻り値
メンバ変数の参照を渡したい場合
 参照や shared_ptr を返す戻り値は const修飾しておこう
shared_ptr 以外では、元のインスタンスの生存期間に注意
 「メンバ変数を外部に晒すなんて・・・」 と一見思うかもしれ
ないが、const修飾しておけば外部からの変更はできない。
const_cast という があるので、絶対ではない
24
class CCMaker{
shared_ptr<ConstCheck> cc_;
public:
//外部からは Read Only な操作しかできない
shared_ptr<ConstCheck const> Get() const{ return cc_; }
};
不変性
コ ン ス ト
破 壊
ブレイカー
戻り値・引数
敬虔なconst教徒は, たとえ値返しでもconstを付ける
 定数・不変ということを強調したい意図がある?
 文法上は、あってもなくても同じ
敬虔なconst教徒は, たとえ値渡しでもconstを付ける
 関数中で同じ変数を使いまわすことは悪である
 ローカル変数と同じ思想
25
boost::optional<double const> const
Div(double const num, double const denom)
{
return denom
? boost::optional<double const>(num / denom)
: boost::none;
}
まとめ
const 付けるとミスが減る!
const 意識するとコードがスッキリする!
const のおかげで なんだか幸せになれました
などなど
縁の下の力持ち!な const さんと
みなさんもこれから仲良くしていきましょう
26

すごいConstたのしく使おう!