初めに
という記事にて、デフォルト引数を使うべきかという議論がコメント欄で発生しました。
@7of9 氏は使うべきではないという立場から
2018-08-23 C++ > デフォルト引数 > 使わない理由(ソースリーディングの観点から)
という記事を出されたので、使うべきという立場から記事を書いてみようと思います。
前提: デフォルト引数とは
templateのデフォルト引数の話もありますが、ここではそうではない方(実行時にわたす引数のほう)を扱います。
つまり
void foo(int n = 0)
{
//do something
}
class bar {
bar(int n = 0){}
};
のような関数についてですね。
STLでの利用例
C++標準ライブラリではどのような使用例があるか見てみることにしましょう。なぜならば、標準ライブラリはすべての基本だからです。C++のバージョンによって若干の差異があるのでここではC++17を見ていきましょう。
string
STLといえばstd::string
だと思うので[要出典]、見てみましょう。
basic_string(const basic_string& str,
size_type pos,
size_type n,
const Allocator& a = Allocator());
コンストラクタは14個もoverloadされています。おもにアロケーターを渡すのにデフォルト引数を利用しているようです。
size_type find_first_of(const basic_string& str, size_type pos = 0) const noexcept;
basic_string& replace(size_type pos1, size_type n1,
const basic_string& str,
size_type pos2, size_type n2 = npos);
- https://cpprefjp.github.io/reference/string/basic_string/find_first_of.html
- https://cpprefjp.github.io/reference/string/basic_string/replace.html
メンバー関数では開始位置指定や長さ指定のパラメータがデフォルト指定になっていることが多いようです。
vector
vector(initializer_list<T> il,
const Allocator& a = Allocator());
コンストラクタは10個もoverloadされています。やはりアロケーターを渡すのにデフォルト引数を利用しています。
LWG 2193: Default constructors for standard library containers are explicit
先程までのクラスのコンストラクタ、よく見ると中にはC++03、C++11時代はデフォルト引数を利用していたものがC++14でoverloadとして分割されています。どういうことでしょうか?
該当のLWGは
https://cplusplus.github.io/LWG/issue2193
です。
デフォルト引数によって引数が0個でも呼び出せるexplicit
指定されたコンストラクタがあるときに、 = {};
のようなコンストラクタ呼び出しができないと書かれています。
class foo {
public:
explicit foo(int n = 0) : n(n) {}
private:
[[maybe_unused]] int n;
};
int main()
{
[[maybe_unused]] foo f;//OK
[[maybe_unused]] foo f2 = {};//error: chosen constructor is explicit in copy-initialization
}
https://wandbox.org/permlink/4Oo3eEa7G3ir0bxJ
つまりこういうことですね。
解決策としてはデフォルト引数を使わずにデフォルトコンストラクタを追加することです。
class foo {
public:
foo() = default;
explicit foo(int n) : n(n) {}
private:
[[maybe_unused]] int n;
};
int main()
{
[[maybe_unused]] foo f;//OK
[[maybe_unused]] foo f2 = {};//OK
}
https://wandbox.org/permlink/PtUPbziqXpTyqrQs
std::vectorのコンストラクタが別件で分割されている
explicit vector(size_type n); // (3) C++11
explicit vector(size_type n,
const Allocator& a = Allocator()); // (3) C++14
vector(size_type n, const T& value,
const Allocator& a = Allocator()); // (4) C++11
explicit vector(size_type n, const T& value = T(),
const Allocator& a = Allocator()); // (3) + (4) C++03
C+++03時代のコンストラクタでは、サイズのみ指定した場合、
- T型のデフォルトコンストラクタを呼び出す
- T型のコピーコンストラクタをn回呼び出して各要素にコピー
というフローになっていました。
ところが一般にコピーコンストラクタの呼び出しコストはデフォルトコンストラクタより大きいのでこれは非効率的でした。
C++11でこれが分割され、C++14ではサイズのみ受け取るコンストラクタにアロケータを渡せなくなっちゃった問題を修正して現在に至ります。
これはコンストラクタの文脈のみではなくすべてのデフォルト引数について考慮するべき事案です。
デフォルト引数を使うということはユーザーにどういうメッセージを発信するか
デフォルト引数は言わずもがな呼び出し時に省略してもいいということなので、そこに任意のパラメータを指定した場合は注意してコードを読んでね、ということを発信しているように思います。
言い換えると予めある社会的合意に反することをやるよ!というメッセージですね。
STLでアロケータがデフォルト引数になっていたのは、通常アロケータを自力で実装して指定するという使い方をしないからです。わざわざアロケーターを指定するということはソースコードを読む人間の注意を喚起しますね。
デフォルト引数をいつ使うべきか
先にほど述べたように、デフォルト引数はユーザーに、省略した状態が一般的だというメッセージを発します。
省略した状態が一般的だという社会的合意は必要で、STLであったりBoostのような影響力の強いライブラリを作るなら十分な調査を元に慎重に検討するべきでしょう。
しかし、我々は普段そんなコードを書くわけではないのでもう少し気軽に考えていいんじゃないかと思います。
例えば @7of9 氏が例に出していたこのコードを見てみよう。
void initDevices()
{
// 初期化(通信速度bps)
initDHT22(1200);
initRHT03(2400);
initSi7021(1200);
initTMP104(2400);
...
}
ここで呼び出している各関数の引数の意味はどうやら通信速度らしいです。
このとき例えばinitDHT22(1200);
の1200
という数字がよく使うなぁと思ったらデフォルト引数を使うべきです。その時その数字の 重要度は低い可能性が高い です。
プログラマーはソースコードを書く時間よりも読む時間のほうが圧倒的に長いので、ソースコードを読むときに どうでもいい情報 で満ち溢れていたら読む速度が落ちてしまい、効率が大きく下がります。
つまりその場合
void initDevices()
{
// 初期化(通信速度bps)
initDHT22();
initRHT03();
initSi7021(1200);
initTMP104(2400);
...
}
のほうがいい、ということになります。
一方でデフォルト値をソースコードを読むときに確認することが多いのだとしたら、それは 重要度が高い情報 ということなのでデフォルト引数を使うべきではありません。
まとめ
まとめると、デフォルト引数はソースコードを読むときにさほど重要ではない情報を遮蔽し、可読性を上げるための機能 であると言えます。
なので積極的に使っていきましょう。
ただし、
- デフォルト引数によって引数が0個でも呼び出せる
explicit
指定されたコンストラクタを作らない - デフォルト値の型のcopy ctor 呼び出しコストがdefault ctor より大きい場合、overloadを追加して分割しデフォルト引数を使わない
を守りましょう。