彷徨えるフジワラ

年がら年中さまよってます

C++ のデフォルト引数挙動

※ 2011/03/24 に追記アリ

Mercurial のパッチを作ろうとした際に、既存クラスのメソッドに引数を加える必要があって、ふと気になった。

デフォルト引数の確定タイミングって、どの時点なんだろう?

例えば、基底クラスのメソッド doit() で引数 arg のデフォルト値が 1 である一方で、派生クラスではデフォルト値が 2 であった場合、どちらの値が採用されるのか?というケース。

まぁ、型ルーズなインタプリタの場合、呼び出されるメソッドの確定契機と同様に、実行時確定しか有り得ないのだけど、では型ストリクトな C++ とかだとどうなるのだろう?という疑問が。

C++ の非 virtual なメソッドの場合、対象の型を元にコンパイル時点で呼び出しメソッドが確定するので:

class Base
{
  public:
    void doit();
};

class Derived
    : public Base
{
  public:
    void doit();
};

上記のようなクラス定義の場合、Base 型変数経由で doit() を呼び出せば、インスタンスが Derived であっても Base::doit() が起動されるし、Derived 型変数経由で doit() を呼び出せば Derived::doit() が起動される。

    Base b1 = Base();
    Derived d1 = Derived();

    b1.doit(); // Base::doit() を実行
    d1.doit(); // Derived::doit() を実行

    Base& b2 = d1;

    b2.doit(); // Base::doit() を実行

その一方で、virtual なメソッドの場合、実行時の対象インスタンスに応じて呼び出すがメソッドが確定することから:

class Base
{
  public:
    virtual void doit();
};

class Derived
    : public Base
{
  public:
    virtual void doit();
};

上記の様なクラス定義の場合、常にインスタンス型に応じたメソッドが呼び出される。

    Base b1 = Base();
    Derived d1 = Derived();

    b1.doit(); // Base::doit() を実行
    d1.doit(); // Derived::doit() を実行

    Base& b2 = d1;

    b2.doit(); // Derived::doit() を実行

ではここで、Derived 側での doit() オーバーライドの際に、引数のデフォルト値を、基底クラスでのそれと違う値にしたとしたら、どうなるだろう?

class Base
{
  public:
    virtual void doit(int arg=1);
};

class Derived
    : public Base
{
  public:
    virtual void doit(int arg=2);
};

考えられる候補としては:

  1. 実行時に、対象インスタンスを元に確定
  2. コンパイル時に、呼び出し対象の型を元に確定
  3. デフォルト引数値の不一致により、コンパイルエラー

呼び出し引数の数を特定する方法を、未だに言語仕様に取り入れていない(よね?)のに、呼び出し関数側での引数有無の判定処理を要する (1) の採用は、ちょっと考え難い。

同一メソッドのデフォルト引数値が異なるのは、実装上の間違い(e.g.: 仕様誤解、修正漏れ)である可能性が高いので、個人的には (3) であって欲しいのだが、いざサンプルコードで確認してみると....やっぱり (2) かぁ! > GCC 3.4.4

警告レベルの関係でメッセージが抑止されているのかも?と思って -Wall を指定してみたけど、特に警告の表示は無し。

1つのメソッド宣言に、実行時インスタンスで確定する要素(= virtual 宣言)と、コンパイル時型情報で確定する要素(= 引数デフォルト値)が混在している、ってーのは、結構紛らわしい気がするなぁ。

追記 @2011/03/24

twitter 経由で以下の様な指摘が < id:koie

デフォルト引数をセットするのはcallerの責務だとおもえば自然かと

呼び出し元(caller)で「デフォルト引数をセット」すること自体は納得済みなんですよ。

「紛らわしい」と表現したのは、virtual 化された関数の、デフォルト引数に関しては:

  • 「処理」は、インスタンスに応じた処理になる
  • 「デフォルト引数値」は、使用する変数型に応じて決まる

という二種類の挙動に関する記述が混在している点なんです。

ひょっとしたら、このような仕様の方が、純粋にオブジェクト指向的には妥当なのかもしれませんが、泥臭い開発の現場に身を置く者としては:

  1. 基底クラスのメソッド宣言において、デフォルト引数値を変更(あるいは、新たにデフォルト引数値指定を導入)
  2. 派生クラスのメソッド宣言は、修正漏れでデフォルト引数値は以前のまま
  3. 派生クラス型の変数経由でメソッド呼び出し
  4. 想定外(= 旧仕様)のデフォルト引数値で動作 ... orz

という状況が容易に想像できて、げんなりしてしまうのです。virtual 化されている場合はとりわけ、うっかり安心してしまって、基底クラス側しか確認しない自分が目に見えますから(笑)。

せめて「これは本当に自覚があってやってるんだよね?」ぐらいの確認をして欲しいんですよ。そういう意味では、派生クラス側で virtual を書き忘れても、特に何も言わずに、しれっと virtual 扱いしてくれるのも個人的には気になるなぁ > コンパイラ

あ、上記のようなガッカリケースを「そんなアホな状況、ある筈ねーよ!」と断言する人は、「自分の能力」と「メンバー編成の幸運」に感謝した方が良いと思います(笑)。