C++のデフォルト引数について

AI実装検定のご案内

C++のデフォルト引数とは、関数の引数にあらかじめ初期値を設定しておき、関数呼び出し時にその引数を省略できる仕組みです。

たとえば、次のように書きます。

#include <iostream>
#include <string>
using namespace std;

void greet(string name = "Guest") {
    cout << "Hello, " << name << endl;
}

int main() {
    greet("Taro");  // Hello, Taro
    greet();        // Hello, Guest
}

greet() のように引数を省略して呼び出すと、name にはデフォルト値である "Guest" が使われます。

デフォルト引数を使うと、よく使う値を毎回書く必要がなくなり、関数呼び出しを簡潔にできます。

目次

デフォルト引数の基本構文

デフォルト引数は、関数の引数に = 値 を指定して定義します。

void func(int x = 10);

この場合、次の2通りの呼び出しができます。

func(5); // x = 5
func();  // x = 10

引数を指定すればその値が使われ、引数を省略すればデフォルト値が使われます。

複数のデフォルト引数

複数の引数にデフォルト値を設定することもできます。

#include <iostream>
#include <string>
using namespace std;

void printInfo(string name = "Unknown", int age = 0) {
    cout << name << ", " << age << endl;
}

int main() {
    printInfo("Alice", 20); // Alice, 20
    printInfo("Bob");       // Bob, 0
    printInfo();            // Unknown, 0
}

このように、デフォルト引数を設定しておくと、必要な引数だけを渡して関数を呼び出せます。

デフォルト引数は右側から指定する

C++では、デフォルト引数を設定する場合、右側の引数から順に設定する必要があります

正しい例

void func(int a, int b = 10, int c = 20);

この関数は、次のように呼び出せます。

func(1, 2, 3); // a = 1, b = 2, c = 3
func(1, 2);    // a = 1, b = 2, c = 20
func(1);       // a = 1, b = 10, c = 20

間違った例

void func(int a = 1, int b, int c = 3); // エラー

このように、左側の引数にデフォルト値を設定したのに、その右側の引数にデフォルト値がない書き方はできません。

C++では、ある引数にデフォルト値を指定した場合、その右側にある引数もすべてデフォルト値を持っている必要があります。

途中の引数だけを省略することはできない

デフォルト引数は、基本的に末尾側の引数から省略します。

void func(int a = 1, int b = 2, int c = 3);

func();        // a = 1, b = 2, c = 3
func(10);      // a = 10, b = 2, c = 3
func(10, 20);  // a = 10, b = 20, c = 3

一方で、次のように「2番目の引数だけを省略して、3番目の引数を指定する」ことはできません。

func(10, , 30); // エラー

C++には、Pythonのような名前付き引数は基本的にありません。

func(a=10, c=30)

このような書き方はC++ではできません。

途中の値だけ変えたい場合

途中の値だけを柔軟に指定したい場合は、オプション用の構造体を使う方法があります。

#include <iostream>
using namespace std;

struct Options {
    int a = 1;
    int b = 2;
    int c = 3;
};

void func(const Options& opt) {
    cout << opt.a << ", " << opt.b << ", " << opt.c << endl;
}

int main() {
    Options opt;
    opt.a = 10;
    opt.c = 30;

    func(opt); // 10, 2, 30
}

C++20以降であれば、指定初期化を使って次のように書くこともできます。

func(Options{
    .a = 10,
    .c = 30
});

この方法なら、どの値を変更しているのかが読みやすくなります。

宣言と定義を分ける場合の注意点

C++では、関数の宣言と定義を分けて書くことがあります。

その場合、デフォルト引数は通常、呼び出し側から見える宣言に書きます

正しい例

#include <iostream>
using namespace std;

void func(int x = 10); // 宣言にデフォルト引数を書く

void func(int x) {     // 定義には書かない
    cout << x << endl;
}

int main() {
    func(); // 10
}

このように、宣言側にデフォルト引数を書いておけば、func() と呼び出したときに 10 が補われます。

間違った例

void func(int x = 10);

void func(int x = 10) { // エラー
    cout << x << endl;
}

同じスコープ内で、同じ引数に対してデフォルト値を複数回指定してはいけません。

ヘッダファイルとソースファイルで使う場合

実務では、関数宣言をヘッダファイルに書き、関数定義をソースファイルに書くことがよくあります。

その場合、デフォルト引数はヘッダファイル側に書くのが基本です。

sample.h

#ifndef SAMPLE_H
#define SAMPLE_H

void printMessage(const char* message = "Hello");

#endif

sample.cpp

#include <iostream>
#include "sample.h"

void printMessage(const char* message) {
    std::cout << message << std::endl;
}

main.cpp

#include "sample.h"

int main() {
    printMessage();          // Hello
    printMessage("Hi C++");  // Hi C++
}

デフォルト引数は、関数本体ではなく呼び出し側で補われる情報です。

そのため、main.cpp から見える sample.h にデフォルト引数を書いておく必要があります。

デフォルト引数は呼び出し側で補われる

デフォルト引数は、関数の中で動的に決まるものではありません。

呼び出し側から見えている宣言に基づいて、コンパイル時に補われます。

void func(int x = 10);

int main() {
    func();
}

この func() は、概念的には次のように扱われます。

func(10);

つまり、デフォルト引数は関数本体に埋め込まれているわけではなく、関数を呼び出す側で補われます。

そのため、ライブラリのヘッダファイルでデフォルト値を変更した場合、呼び出し側の再コンパイルが必要になることがあります。

デフォルト引数の式と評価タイミング

デフォルト引数には、定数だけでなく関数呼び出しなどの式も指定できます。

#include <iostream>
using namespace std;

int getDefault() {
    return 100;
}

void func(int x = getDefault()) {
    cout << x << endl;
}

int main() {
    func(); // getDefault() の結果が使われる
}

式は呼び出し時に評価される

デフォルト引数の式は、引数を省略して関数を呼び出したときに評価されます。

#include <iostream>
using namespace std;

int counter = 0;

int nextValue() {
    return ++counter;
}

void func(int x = nextValue()) {
    cout << x << endl;
}

int main() {
    func();    // 1
    func();    // 2
    func(100); // 100
    func();    // 3
}

この例では、func() として引数を省略したときだけ nextValue() が呼ばれます。

そのため、出力は次のようになります。

1
2
100
3

名前探索は宣言時、評価は呼び出し時

デフォルト引数で使われる名前は、デフォルト引数を書いた宣言の時点で解決されます。

ただし、その式の実行自体は、関数を呼び出したときに行われます。

#include <iostream>
using namespace std;

int value = 10;

int getValue() {
    return value;
}

void func(int x = getValue()) {
    cout << x << endl;
}

int main() {
    value = 20;
    func(); // 20
}

この例では、getValue という名前は func の宣言時に解決されます。

しかし、getValue() が実行されるのは func() を呼び出したときです。

そのため、value20 に変更した後に func() を呼ぶと、20 が出力されます。

ローカル変数はデフォルト引数に使えない

デフォルト引数には、通常、ローカル変数を使えません。

void outer() {
    int local = 10;

    void func(int x = local); // エラー
}

関数の引数も同様に、デフォルト引数には使えません。

void outer(int n) {
    void func(int x = n); // エラー
}

一方で、グローバル変数はデフォルト引数に使えます。

#include <iostream>
using namespace std;

int defaultValue = 10;

void func(int x = defaultValue) {
    cout << x << endl;
}

int main() {
    func(); // 10

    defaultValue = 20;
    func(); // 20
}

この例では、func() を呼び出すたびに defaultValue の現在の値が使われます。

デフォルト引数と関数オーバーロード

C++では、同じ名前で引数の異なる関数を定義できます。

これを関数オーバーロードといいます。

void func(int x);
void func(double x);

デフォルト引数とオーバーロードを組み合わせると、呼び出しが曖昧になることがあります。

曖昧になる例

#include <iostream>
using namespace std;

void func(int x = 10) {
    cout << "func(int)" << endl;
}

void func() {
    cout << "func()" << endl;
}

int main() {
    func(); // エラー:どちらを呼ぶべきか曖昧
}

func() と呼び出したとき、次の2つの関数がどちらも候補になります。

void func(int x = 10);
void func();

そのため、コンパイラはどちらを呼ぶべきか判断できません。

デフォルト引数を使う場合は、オーバーロードと組み合わせて曖昧な呼び出しにならないように注意が必要です。

デフォルト引数と関数ポインタ

デフォルト引数は、関数の型には含まれません。

void func(int x = 10);

この関数の型は、あくまで次の型です。

void(int)

void() ではありません。

そのため、関数ポインタ経由ではデフォルト引数を使えません。

#include <iostream>
using namespace std;

void func(int x = 10) {
    cout << x << endl;
}

int main() {
    void (*p)(int) = func;

    p(20); // OK
    // p(); // エラー
}

func(); は呼び出せますが、p(); は呼び出せません。

デフォルト引数は、関数ポインタ型に含まれる情報ではなく、関数宣言に結びついた情報だからです。

デフォルト引数と仮想関数

デフォルト引数と仮想関数を組み合わせる場合は、特に注意が必要です。

仮想関数では、実際に呼ばれる関数本体は実行時に決まります。

一方で、デフォルト引数はコンパイル時に、呼び出し式の静的型に基づいて決まります。

注意が必要な例

#include <iostream>
using namespace std;

class Base {
public:
    virtual void show(int x = 10) {
        cout << "Base: " << x << endl;
    }
};

class Derived : public Base {
public:
    void show(int x = 20) override {
        cout << "Derived: " << x << endl;
    }
};

int main() {
    Derived d;
    Base* p = &d;

    p->show(); // Derived: 10
}

出力は次のようになります。

Derived: 10

p の静的型は Base* なので、デフォルト引数は Base::show10 が使われます。

しかし、show は仮想関数なので、実際に呼ばれる関数本体は Derived::show です。

つまり、次の呼び出しは、

p->show();

概念的には次のように補われます。

p->show(10);

その後、実行時ディスパッチによって Derived::show(10) が呼ばれます。

仮想関数ではデフォルト値を統一する

仮想関数で、基底クラスと派生クラスに異なるデフォルト引数を書くと混乱の原因になります。

class Base {
public:
    virtual void show(int x = 10);
};

class Derived : public Base {
public:
    void show(int x = 20) override;
};

このような書き方は避けた方が安全です。

実務では、デフォルト引数を基底クラス側にだけ書き、派生クラス側では書かない方がわかりやすくなります。

class Base {
public:
    virtual void show(int x = 10);
};

class Derived : public Base {
public:
    void show(int x) override;
};

メンバ関数のデフォルト引数

クラスのメンバ関数にも、デフォルト引数を指定できます。

#include <iostream>
#include <string>
using namespace std;

class Printer {
public:
    void print(string message = "Hello") {
        cout << message << endl;
    }
};

int main() {
    Printer p;

    p.print("Hi"); // Hi
    p.print();     // Hello
}

メンバ関数でも、通常の関数と同じように、引数を省略するとデフォルト値が使われます。

メンバ関数をクラス外で定義する場合

メンバ関数をクラス外で定義する場合、実務ではデフォルト引数をクラス内の宣言に書くのが基本です。

#include <iostream>
#include <string>
using namespace std;

class Printer {
public:
    void print(string message = "Hello");
};

void Printer::print(string message) {
    cout << message << endl;
}

int main() {
    Printer p;
    p.print(); // Hello
}

定義側にもう一度同じデフォルト値を書くことはできません。

class Printer {
public:
    void print(string message = "Hello");
};

void Printer::print(string message = "Hello") { // エラー
    cout << message << endl;
}

なお、C++の仕様上は、非テンプレートクラスのメンバ関数において、クラス外定義で未指定のデフォルト引数を追加できる場合があります。

ただし、読みづらくなりやすいため、実務ではクラス内宣言にまとめて書く方が安全です。

コンストラクタのデフォルト引数

コンストラクタにもデフォルト引数を使えます。

#include <iostream>
#include <string>
using namespace std;

class User {
private:
    string name;
    int age;

public:
    User(string n = "Guest", int a = 0)
        : name(n), age(a) {}

    void show() {
        cout << name << ", " << age << endl;
    }
};

int main() {
    User u1("Alice", 20);
    User u2("Bob");
    User u3;

    u1.show(); // Alice, 20
    u2.show(); // Bob, 0
    u3.show(); // Guest, 0
}

このように、コンストラクタの引数にデフォルト値を設定すると、オブジェクト生成時に一部またはすべての引数を省略できます。

デフォルトコンストラクタとの関係

すべての引数にデフォルト値を持つコンストラクタは、引数なしでも呼び出せます。

#include <string>
using namespace std;

class User {
public:
    User(string name = "Guest") {}
};

int main() {
    User u; // OK
}

ただし、別に引数なしコンストラクタも定義すると、呼び出しが曖昧になる場合があります。

#include <string>
using namespace std;

class User {
public:
    User() {}
    User(string name = "Guest") {}
};

int main() {
    User u; // エラー:どちらを呼ぶべきか曖昧
}

User() も呼べますし、User(string name = "Guest") も引数なしで呼べるためです。

デフォルト引数と nullptr

ポインタ引数にデフォルト値を設定する場合は、NULL よりも nullptr を使うのが現代的です。

#include <iostream>
using namespace std;

void process(int* p = nullptr) {
    if (p == nullptr) {
        cout << "null" << endl;
    } else {
        cout << *p << endl;
    }
}

int main() {
    int x = 10;

    process(&x); // 10
    process();   // null
}

nullptr はポインタの空値を表すために用意されたキーワードで、NULL よりも型安全です。

デフォルト引数と参照

参照引数にもデフォルト引数を設定できます。

ただし、const 参照と非 const 参照では扱いが異なります。

const 参照の場合

const 参照は一時オブジェクトを参照できるため、次のコードは正しく動作します。

#include <iostream>
#include <string>
using namespace std;

void print(const string& s = "default") {
    cout << s << endl;
}

int main() {
    print("hello");
    print();
}

print() と呼び出した場合、"default" から一時的な std::string オブジェクトが作られ、それを const string& が参照します。

const 参照の場合

一方、非 const 参照は一時値を参照できません。

そのため、次のコードはエラーになります。

void func(int& x = 10); // エラー

ただし、グローバル変数のような左辺値であれば、非 const 参照のデフォルト引数として使えます。

int defaultValue = 10;

void func(int& x = defaultValue) {
    x++;
}

この場合、func() を呼ぶと defaultValue が変更されます。

ただし、副作用がわかりにくくなるため、実務では慎重に使うべきです。

デフォルト引数とテンプレート

関数テンプレートにも、デフォルト引数を指定できます。

#include <iostream>
#include <string>
using namespace std;

template <typename T>
void printValue(T value = T{}) {
    cout << value << endl;
}

int main() {
    printValue<int>();    // 0
    printValue<double>(); // 0
    printValue<string>(); // 空文字
}

この例では、T{} によって型ごとの値初期化が行われます。

型推論には注意する

テンプレートでは、型推論に注意が必要です。

template <typename T>
void func(T value = T{}) {}

func(); // エラー

この場合、引数がないため、コンパイラは T が何かを推論できません。

そのため、次のようにテンプレート引数を明示する必要があります。

func<int>();

デフォルト引数とラムダ式

C++では、ラムダ式にもデフォルト引数を指定できます。

#include <iostream>
using namespace std;

int main() {
    auto f = [](int x = 10) {
        cout << x << endl;
    };

    f(20); // 20
    f();   // 10
}

複数の引数を持つラムダでも、通常の関数と同じように、右側の引数からデフォルト値を設定します。

#include <iostream>
using namespace std;

int main() {
    auto f = [](int a, int b = 10) {
        cout << a + b << endl;
    };

    f(5);    // 15
    f(5, 2); // 7
}

デフォルト引数を使うべき場面

デフォルト引数は、関数の使いやすさを高める便利な機能です。

特に、よく使う値や自然な初期値がある場合に向いています。

省略されることが多いオプション値がある場合

void log(string message, bool newline = true);

通常は改行ありで出力し、必要なときだけ false を指定できます。

log("Hello");
log("Loading...", false);

自然な標準値がある場合

void connect(string host, int port = 80);

HTTPの標準ポートのように、多くの場合で同じ値を使う引数には、デフォルト引数が向いています。

既存関数にオプションを追加したい場合

既存の関数に後からオプションを追加したい場合にも、デフォルト引数は便利です。

void save(string filename, bool overwrite = false);

このようにすれば、既存の呼び出しコードを大きく変えずに、新しいオプションを追加できます。

ただし、公開ライブラリでは、呼び出し側の再コンパイルやバイナリ互換性に注意が必要です。

デフォルト引数を避けた方がよい場面

デフォルト引数は便利ですが、多用すると関数の意味がわかりにくくなる場合があります。

引数の意味が読み取りにくい場合

次のような呼び出しは、コードを見ただけでは意味がわかりにくくなります。

createUser("Taro", true, false, 3);

関数宣言を見ると意味はわかります。

void createUser(
    string name,
    bool isAdmin = false,
    bool sendEmail = true,
    int retryCount = 3
);

しかし、呼び出し側だけを見ると、truefalse が何を表しているのかわかりません。

このような場合は、オプション用の構造体を使う方が読みやすくなります。

struct CreateUserOptions {
    bool isAdmin = false;
    bool sendEmail = true;
    int retryCount = 3;
};

void createUser(string name, const CreateUserOptions& options = {});

呼び出し側は次のように書けます。

CreateUserOptions options;
options.isAdmin = true;
options.sendEmail = false;

createUser("Taro", options);

C++20以降であれば、指定初期化を使ってさらに読みやすくできます。

createUser("Taro", CreateUserOptions{
    .isAdmin = true,
    .sendEmail = false
});

引数の数が多すぎる場合

次のように、多数のデフォルト引数を持つ関数は読みにくくなりがちです。

void draw(
    int x = 0,
    int y = 0,
    int width = 100,
    int height = 100,
    bool visible = true,
    bool border = false,
    int layer = 0
);

呼び出し側も次のようにわかりにくくなります。

draw(10, 20, 300, 200, true, false, 2);

このような場合も、オプション構造体を検討した方がよいです。

オーバーロードの方が自然な場合

引数の有無や型によって処理の意味が大きく変わる場合は、デフォルト引数よりもオーバーロードの方が自然です。

void openFile(string path);
void openFile(string path, string mode);

また、次のように処理の目的が異なる場合は、関数名を分けた方がさらに読みやすくなります。

void loadFromFile(string path);
void loadFromMemory(const char* data, size_t size);

デフォルト引数とオーバーロードの使い分け

デフォルト引数とオーバーロードは、どちらも関数呼び出しを柔軟にするための機能です。

ただし、向いている場面は異なります。

デフォルト引数が向いている場合

同じ処理で、一部のオプション値だけを省略したい場合は、デフォルト引数が向いています。

void print(string text, int repeat = 1);

この関数では、repeat を省略した場合に1回だけ出力する、という自然な意味になります。

オーバーロードが向いている場合

引数の種類や数によって処理の意味が変わる場合は、オーバーロードが向いています。

void setValue(int value);
void setValue(string value);

また、処理内容が大きく異なる場合は、オーバーロードよりも関数名を分けた方がよい場合もあります。

void loadFromFile(string path);
void loadFromMemory(const char* data, size_t size);

デフォルト引数でよくあるミス

デフォルト引数には、初心者がつまずきやすいポイントがあります。

左側の引数だけにデフォルト値をつける

void func(int a = 1, int b); // エラー

デフォルト引数は右側から設定する必要があります。

修正すると、次のようになります。

void func(int a, int b = 1); // OK

宣言と定義の両方にデフォルト値を書く

void func(int x = 10);

void func(int x = 10) { // エラー
}

デフォルト引数は、同じスコープ内で重複して指定できません。

修正すると、次のようになります。

void func(int x = 10);

void func(int x) {
}

オーバーロードと組み合わせて曖昧にする

void func();
void func(int x = 10);

func(); // エラー

この場合、どちらの func を呼ぶべきか曖昧になります。

修正するなら、どちらか一方の設計に寄せます。

void func();
void func(int x);

または、次のようにデフォルト引数だけにします。

void func(int x = 10);

仮想関数で派生クラス側だけデフォルト値を変える

class Base {
public:
    virtual void show(int x = 10);
};

class Derived : public Base {
public:
    void show(int x = 20) override;
};

このコードはコンパイルできる場合がありますが、動作が直感とずれる原因になります。

仮想関数でデフォルト引数を使う場合は、基底クラス側に統一するのが安全です。

class Base {
public:
    virtual void show(int x = 10);
};

class Derived : public Base {
public:
    void show(int x) override;
};

実務でのおすすめルール

デフォルト引数を安全に使うには、いくつかのルールを意識するとよいです。

デフォルト引数は呼び出し側から見える宣言に書く

ヘッダファイルとソースファイルを分ける場合は、ヘッダ側に書きます。

// .h
void func(int x = 10);

// .cpp
void func(int x) {
}

デフォルト引数は右側から設定する

void func(int a, int b = 10, int c = 20);

左側だけにデフォルト値を設定することはできません。

仮想関数ではなるべく避ける

仮想関数では、デフォルト引数と実際に呼ばれる関数本体が別々に決まります。

そのため、仮想関数でデフォルト引数を使う場合は、基底クラス側に統一するのが安全です。

class Base {
public:
    virtual void show(int x = 10);
};

class Derived : public Base {
public:
    void show(int x) override;
};

bool のデフォルト引数が増えたら設計を見直す

次のような関数は、呼び出し側が読みにくくなりがちです。

void save(bool overwrite = false, bool compress = true);

呼び出し側だけを見ると、意味がわかりにくくなります。

save(true, false);

このような場合は、オプション構造体を使う方が読みやすくなります。

struct SaveOptions {
    bool overwrite = false;
    bool compress = true;
};

void save(const SaveOptions& options = {});

まとめ

C++のデフォルト引数は、関数の引数にあらかじめ値を設定しておき、呼び出し時に省略できるようにする機能です。

void func(int x = 10);

func();  // x = 10
func(5); // x = 5

デフォルト引数を使うと、よく使う値を毎回指定する必要がなくなり、関数呼び出しを簡潔にできます。

ただし、使い方にはいくつか注意点があります。

ポイント内容
基本構文引数 = 値 の形で指定する
省略方向末尾側の引数から省略できる
宣言と定義呼び出し側から見える宣言に書く
ヘッダ利用ヘッダファイルにデフォルト引数を書く
評価タイミング省略された呼び出し時に式が評価される
名前探索デフォルト引数を書いた宣言時に行われる
オーバーロード曖昧な呼び出しに注意する
仮想関数デフォルト引数は静的型、関数本体は動的型で決まる
関数ポインタデフォルト引数は関数型に含まれない
設計上の注意引数が多い場合はオプション構造体を検討する

デフォルト引数は、少数の自然なオプション値に使うと非常に便利です。

一方で、引数が多すぎる関数や、bool が並ぶ関数、仮想関数との組み合わせでは、コードの意味がわかりにくくなることがあります。

実務では、「省略しても自然に意味が通じる値」に限定して使うのが安全です。

以上、C++のデフォルト引数についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次