見出し画像

C++ 再入門 その7 関数の多重定義(オーバーロード)

前回、コンストラクタという特別なクラスの関数で、同じ名前で引き数の数や型が異なる関数をいくつでも書くことができるという説明をしました。このように同じ名前でいくつもの関数を定義することを関数の多重定義またはオーバーロードと呼びます。

これはC++と特徴のひとつでC言語と大きく異なる機能です。これはコンストラクタに限らず、どんな関数に対しても当てはまります。クラスの関数でなくても同じです。

C言語では関数ごとに引き数の数と、それぞれの型はひとつだけしか許されず、これが合っていなければエラーとなってしまいます。それに引き換えC++では、引き数が合致する関数をコンパイラが見つけて、ちゃんとそれを呼んでくれるのです。ですからウッカリ引き数の型を間違えてしまった場合、もしその間違えた関数が定義されていれば、思ってもなかった「正しい」関数がエラーにならず何事も無かったように呼び出されるのです。

具体的な例を挙げましょう。良くマクロで実装されている2つの値の大きい値を返す max関数を考えます。C言語の場合は関数の多重定義が出来ないので、型ごとに imax であるとか dmax などと名前を変える(マクロで実装されるのはあくまで文字として展開するので引き数の型が関係ないため)のですが、C++ではオーバーロードが使えるので max と同じ名前の関数で実装できます。

int max(int i, int j) {  return ( i > j ? i : j);}
double max(double, double) { return (i > j ? i : j) }

これらの関数を定義しておけば、

#include <iostream>
using namespace std;
int max(int i, int j) {  return ( i > j ? i : j); }
double max(double x, double y) { return (x > y ? x : y); }

void main() {
  cout << max(1, 5) << endl;
  cout << max(1.5, 3.8) << endl;
}

5
3.8

実行結果

さて、ここまでは簡単です。ちょっと意地悪なコードを出してみます。

max(015,10)

の結果はどうなるでしょうか?0で始まる数値は8進であると解釈されるので結果は13です。今度はリテラルにサフィックスを付けてみましょう。

max(0x7FFFFFFFFFFFL, 0)

どうやら、これは0を返しますね。ここはC言語と同じ世界。0xプレフィックスで16進数を解釈するだけでなく本来は unsigned な筈なんですけどね。ここは通常の型変換ルールで int に変換されて該当する int型の max が呼ばれるわけです。この時に型変換されるか対応する定義を見つけられるかは、あいまいなところがあり、コンパイラが困って「わからん、キャストしてくれ」というエラーを返すことがあります。まあ人間が理解できない型変換をしてしまうよりはお利口ですが、型変換が起こるような型でオーバーロードするのは結構、危険です。

さて、次はいろいろな型の値を表示に使うなどの目的で文字列に変換する関数を書いてみましょう。いろいろな型に対して同じ名前の関数が使えるのは便利ですよね。実は to_string という便利な関数があるので、それを安易に使います。

string to_str(int i) { return to_string(i); }
string to_str(double x) { return to_string(x); }
string to_str(char c) { return string(1, c); }
string to_str(const char *p) { return string(p); }

これで、to_str(10)、 to_str(1.0)、 to_str(’A’)、 to_str(”ABC”) がちゃんと出力できるはずです。実はオーバーロード関数の検索で const 付きと無しはハッキリと区別されるので、to_str(char *p) だと文字列リテラル(型としては const char* で解釈される)を引き数にするとエラーになったりします。この辺りはC言語より const の意味が大きくなっています(const のある無しは string テンプレートにとっては大きな違い)。

さて、オーバーロードでは無いのですが、C++には引き数を省略すると決まった値を渡されたと解釈される機能があります。引き数の型と名前の後ろに"="に続き省略時に使われる値を書きます。

#include <stdlib.h>

// Visual Studio の場合
#pragma warning(disable : 4996)

string to_str(int i, int radix = 10) { 
  char buf[32];
  return string(_itoa(i, buf, radix));
}

ちょっと手抜きのコードで申し訳ありませんが、これで今まで通り to_str(15) で”15”が得られ、to_str(15, 16) とすれば”f”が返ってきます。基数の種類はitoa 任せなのですが、ここは自力できちんと書くことも出来るはずなので、挑戦してみてください。

この基数に対応したオーバーロードした関数と、今までの引き数がひとつの関数の両方を定義することは出来るのですが(定義することでエラーにはならない)、ここで引き数がひとつしかない呼び出しがあると、どちらを選んで良いのかをコンパイラは判断できないのでエラーになります。なかなか便利なのかややこしいだけなのか難しいところですね。

実はオーバーロードで出来ないこともあるんです。それは引き数がすべて同じで戻り値の型が異なる関数です。関数を呼び出す際の引き数の数と型で定義されている関数を探すので、戻り値の型は見ていないのです。もし戻り値の型だけが違う関数を定義すると、それはエラーとなるのです。見つかった関数で戻り値の型が決まり、その結果が代入されるのであれば通常の型変換が行われます。景気よくいくつもの同じ名前の関数を書いていると、うっかり全部同じ引き数の関数を書いてしまって「ヤバ」となることもママあります。

このオーバーロードは、いろいろなポインタ型を引数に取るときや、継承されたクラスをひとまとめに扱う時に無くてはならないものなのですが、この型変換と関数の選択のルールを理解していないと、思わぬところでエラーが発生して「コンパイルできない=動かせない」ことになり頭を悩ますことになります。

このようにC++には多くの便利な機能がたくさんあるのですが、C言語に比べて型にシビアで、C++のルールを正確に理解していないと、すぐにコンパイルエラーとなってしまいます。ここは習うより慣れろで実際にエラーを出して、何が悪かったのかを体で覚えるしかありません。ルールだけを追っていてもなかなか理解が進みません。実のところ、コンパイラが何を基準にコードを作っているのかの部分までわかるようになれば、かなりスッキリするような気もするのですが、そこまで到達するにはかなりの経験と知識が必要なので、根気よくコードを書き続けましょう。

さて、次回はクラスの使い方に戻ります。

ヘッダ画像は、以下のものを使わせていただきました。
https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos, パブリック・ドメイン, https://commons.wikimedia.org/w/index.php?curid=62851110による

#多重定義 #オーバーロード #引き数の型 #省略時解釈 #型変換 #constの有無

この記事が気に入ったらサポートをしてみませんか?