さようなら暗黙の型変換

はじめに

C++20で導入予定であった契約プログラミングをご存じでしょうか.
契約プログラミングで出来ることの一つとして,関数の引数に事前条件を付与できるというのがあります.
この機能を用いることでより安全な関数を呼び出しを実現できます..
初めてこの機能を知った時は非常に感動していたのですが,一つの穴に気付いてしまいました.
それが暗黙の型変換です.
暗黙の型変換が行われた場合,関数が呼び出される前に意図せぬ動作を起こすため契約プログラミングではおそらく対処ができません.
そこで本記事では関数呼び出しの際に発生する暗黙の型変換への対処について執筆していこうと思います.
なお,残念ながら契約プログラミングC++20での導入が見送られてしまったようです.
今後,機能が強化されて再び姿を現すと思われるので気長に待ちましょう.

問題

まずは実際に起こり得る問題の例を見てみましょう.

#include <iostream>

constexpr int func(int n) {
	return n * n;
}

signed main() {
	constexpr auto val = func(5.5);
	std::cout << val << std::endl;
}

この例では,int型しか受け取らないはずの関数にfloat値を渡すことが出来てしまい意図しない動作が起こっています.
このように,関数の呼び出しの際に暗黙の型変換が起こることで,関数設計者の想定と異なる挙動が起こることがあります.

constexpr int func(float n) {
	return -1; // ERROR
}

上記コードのようにオーバーロードすることで解決はできますが,暗黙の型変換が起こり得る全てのパターンをオーバーロードしきるのは困難です.

解決

前節の結果から「intの場合のみ想定の関数を呼び,それ以外は呼び出せないようにする」というのを実現できれば解決できそうです.

可変長引数を用いたオーバロード

前節ではそれ以外を一つ一つのオーバロードで解決しようとしましたが,全て一挙にオーバロードする方法があります.
それが,可変長引数です.
可変長引数を用いて関数を定義すると,明示的に定義された関数が呼べない全ての場合で可変長引数を用いた関数を呼ぶようにできます.

constexpr void f1() {}
constexpr void f1(int a) {}
constexpr void f1(int a, int b) {}
constexpr void f1(...) {} // ↑のどれも呼ばれない場合に呼ばれる

この仕組みを用いて最初のコードを書きなおします.

#include <iostream>
#include <cassert>

constexpr int func(int n) {
	return n * n;
}

constexpr int func(...) { assert(false); return -1; }

signed main() {
	constexpr auto val = func(5.5);
	std::cout << val << std::endl;
}

これを実行すると25が出力されます.
どうやらこれではまだ暗黙の型変換が行われてしまうようです.
実はこれはオーバーロードの優先順位による現象です.

オーバーロードの優先順位

C++では関数をオーバーロードする手段をいくつか存在します.
じつはこれらのオーバーロードには優先順位がもう設けられており,
複数のオーバーロード候補が存在する場合はより優先順位の高い関数を使用するようになっています.
優先順位は以下のようになっています.

  1. 通常の呼び出し(修飾込)
  2. 特殊化テンプレート
  3. テンプレート
  4. 暗黙的な型変換で呼び出し可能なもの
  5. 可変長引数

今回防止したい暗黙の型変換での呼び出しは優先度4であり,前節の可変長引数は優先度5です.
この優先度の違いにより,前節では暗黙の型変換での呼び出しが呼ばれてしまっていたのです.

テンプレートを用いたオーバーロード

可変長引数では優先度が低く対処ができなかったので,暗黙的な型変換で呼び出しよりも優先度の高いテンプレートを用いた解決を試みます.
前々節の可変長引数関数をテンプレート関数に置き換えてみましょう.

#include <iostream>
#include <cassert>

constexpr int func(int n) {
	return n * n;
}

template<class T>
constexpr int func(T) { assert(false); return -1; }

signed main() {
	constexpr auto val = func(5.5);
	std::cout << val << std::endl;
}

実行(コンパイル)するとassertが呼ばれていることが確認できました.
無事に解決できました.

より汎用的な解決

テンプレートを用いたオーバーロードを駆使することで問題を解決することが出来ましたが,まだ問題は残っています.
今回の例では引数を一つだけ取る関数のみでしたが,引数を複数個取る関数がいくつかあった場合はどうでしょうか.
このような場合に暗黙の型変換を防ぐにはそれぞれの引数の個数に対応するテンプレートを記述する必要があります.
下記のコードの場合は3つのテンプレートオーバーロードが必要となります.
これではまだ関数設計に解決方法が依存してしまい,少し面倒くさいことが起こりかねません.

constexpr int func(int n) { return n; }
constexpr int func(int n, int m) { return n * m; }
constexpr int func(int n, int m, int l) { return n * m * l; }

そこでより汎用的な,どのような場合でも解決できる方法が可変引数テンプレートです.
テンプレート引数を可変にすることで,あらゆる数の引数に対応することができます.
以下のように記述します.

template<class... Args>
constexpr int func(Args...) { assert(false); return -1; }

おわりに

暗黙の型変換による関数呼び出しを防ぐ方法として,可変引数テンプレートを用いたオーバーロードを紹介しました.
これはあらゆる関数に対しても全く同じ記述で適用できるので個人的には便利だと思っています.

ただそもそも暗黙の型変換させないようにする方法とかありそうな感じがしますけどないんですかね...?
知っている方がいれば教えて頂きたいです.