この記事で扱うこと
Rustには関数オーバーロード機能がありません。(演算子オーバーロードはあります。)
C++などで関数オーバーロードを使いまくっていた人がRustを使い始める場合、どうすればいいでしょうか?
結論から先に書くと
- 頑張れば(トレイトを活用すると)Rustで関数オーバーロードを実質的に再現することはある程度可能
- 関数オーバーロード記法自体を再現してくれるクレートも存在する(が、無理やりなので非推奨)
- そもそもRustでは関数オーバーロードではなくトレイトを活用することが想定されている
- Rustにはコンストラクタが無いのでコンストラクタのオーバーロードも無く、関数名を書き分けることが推奨されている
- C++のテンプレート関数における特殊化と同等の機能はRustのトレイトでは発展途上(nightlyで一部サポート)
- C++のSFINAEやC++20のコンセプト (concepts)の柔軟性と比べるとRustのトレイトは窮屈
- C++のvariadic templatesのような可変長引数関数の機能もRustにはない
- 総じて言えばRustに関数オーバーロードが無いことは(多くのユーザーにとって)気になるレベルではない
Rustには関数オーバーロードが無い
Rustには関数オーバーロード機能が無いので以下のコードはコンパイルエラーになります。
fn foo() {
println!("void!!");
}
fn foo(n: i32) {
println!("i32!!");
}
// ↑ 2つ目のfooを定義するとエラーになる!!
// error[E0428]: the name `foo` is defined multiple times
Rustのトレイトは高機能
Rustのドキュメントではトレイトについて以下のように書かれています。
違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
確かにインターフェースとしての機能がメインなのですが、実際にはもっと高機能で、以下のように外部で定義されている型やプリミティブ型、タプル型に対しても新たにメソッドを生やすことができます。
trait Foo {
fn foo(&self);
}
impl Foo for Vec<i32> {
fn foo(&self) {
println!("vector!!!")
}
}
impl Foo for i32 {
fn foo(&self) {
println!("int!!!");
}
}
impl Foo for (i32, i32) {
fn foo(&self) {
println!("tuple!!!")
}
}
fn main() {
let v = vec![1, 2, 3];
v.foo(); // vector!!!
1.foo(); // int!!!
(1, 2).foo(); // tuple!!!
もしあなたがRust初学者ならこのコードに驚きませんか?自分は最初これを知ったとき驚きました。
(こんな勝手にメソッドを生やせるのは危険だと思われるかもしれませんが、影響は自分が書いているクレート内に限定されるので安全です。)
そして上記のコードにFooトレイトを引数に持つ関数fooの定義を追加すると、関数オーバーロード(ぽいもの)を作れます。
fn foo(f: &impl Foo) {
f.foo();
}
fn main() {
let v = vec![1, 2, 3];
foo(&v); // vector!!!
foo(&1); // int!!!
foo(&(1, 2)); // tuple!!!
}
もちろんこれはトレイトによってインターフェースの実装をそれぞれの型に付与しているわけで(引数の数の違いもタプル型で表現しているだけですし引数0個のfooは定義できないので)、関数オーバーロードというより静的ポリモーフィズム(静的ディスパッチ)といった方が正しいと思いますが、ともかく関数オーバーロードで実現したかったことはそれなりに再現できるということです。
C++やJavaではインターフェースでこのような芸当ができないので関数オーバーロードに価値があると思いますが、Rustはこのように高機能なトレイトがあるので、Rustに関数オーバーロードは不要であるという考え方には理があると思います。(なので、Rustで関数オーバーロードにこだわり続けている人は少ないように見えます。)
しかしここで、C++のテンプレートメタプログラミングを活用した関数オーバーロードを使っていた人にとっては、ジェネリクスを使ったトレイトでどれだけ同等の機能を実現できるのか?という疑問が生じると思います。
そこで次の節に行きます。
RustではC++レベルの関数オーバーロードをどこまで再現できるのか?
Rustの話に入る前に、そもそもC++では関数オーバーロードをどのように活用できるのかに触れたいと思います。
C++の関数オーバーロードではどんなことができるのか
以下のコードでは、stringやvector、map、unordered_map、set、またはそれらの任意の入れ子構造のインスタンスの値であるstring中の文字を全て"*"に置き換える関数maskを定義しています。
これらを実現するために、イテレータを持つクラスを表現するiterableと、文字列であるstring、それからmapの要素であるpairを引数に取る複数のmask関数をオーバーロードしています。
ポイントはvectorやmapの入れ子構造に対応するため各オーバーロード関数からさらにオーバーロード関数を再帰的に呼び出していることです。これは関数名が同一であるからこそ実現できるものです。
したがって関数オーバーロードではなく別名の関数を作る、という代替手段はこのケースでは採用できません。
#include <iostream>
#include <utility>
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <set>
#include <algorithm>
template<typename T>
concept iterable = requires(T& t) {
std::begin(t);
std::end(t);
};
template<typename T>
T mask(const T& t) {
return T(t);
}
std::string mask(const std::string& str) {
return std::string(str.length(), '*');
}
// iterable引数とpair引数のmask関数は相互に呼び出されるので少なくとも片方は先に宣言が必要
template<iterable T>
T mask(const T& iterable);
template<typename T, typename U>
std::pair<T, U> mask(const std::pair<const T, U>& p) {
return std::pair<T, U>{p.first, mask(p.second)};
}
template<iterable T>
T mask(const T& iterable) {
T masked;
std::transform(std::begin(iterable), std::end(iterable), std::inserter(masked, std::end(masked)),
[](const T::value_type& t) -> T::value_type {return mask(t);});
return masked;
}
int main() {
std::string str = std::string("foo");
auto masked_str = mask(str); // "***"
std::vector<std::string> vec = {"bar", "buzz"};
auto masked_vec = mask(vec); // {"***", "****"}
std::vector<std::vector<std::string>> vec2d = {{"0", "12"}, {"abcd", "efghijk"}};
auto masked_vec2d = mask(vec2d); // {{"*", "**"}, {"****", "*******"}}
std::map<std::string, std::string> mp = {{"key1", "012-3456-7890"}, {"key2", "123-4567-8901"}};
auto masked_mp = mask(mp); // {{"key1", "*************"}, {"key2", "*************"}}
std::map<std::string, std::vector<std::string>> map_vec = {{"person1", {"012-3456-7890", "123-4567-8901"}}};
auto masked_map_vec = mask(map_vec); // {{"person1", {"*************", "*************"}}}
std::unordered_map<std::string, std::string> ump = {{"key1", "012-3456-7890"}, {"key2", "123-4567-8901"}};
auto masked_ump = mask(ump); // {{"key1", "*************"}, {"key2", "*************"}}
std::set<std::string> st = {"1", "22", "333"};
auto masked_set = mask(st); // {"*", "**", "***"}
}
main関数のコードとコメントを見れば分かるように、C++ではテンプレート関数のオーバーロードを使うとコンパイル時に作られた様々な型に対してmask関数を適用できるのです。
このような関数オーバーロードが実現できるのは、C++がテンプレートの特殊化と関数オーバーロード解決の優先順位付け(複数の関数オーバーロードに適合しても、実際に呼び出すのは最も適合度の高い関数となる仕組み)に対応しているからです。
また、C++にはもともとSFINAEという仕様があることで関数シグネチャ部分の記述は非常に自由度が高く、Rustでは即座にコンパイルエラーになるような書き方もたくさんできます。
Rustではどこまで頑張れるのか(implブロックの特殊化機能が無いことに注目)
Rustではトレイトのimplブロックの特殊化機能(specialization)が無い(関数オーバーロードで言い換えると、複数の関数オーバーロードに適合することが許されない)ので、C++のように書くことはできません。
Rustのnightlyバージョンではこのimplブロック特殊化機能の実装が進められていますが、設計上の課題やバグが多くまだ発展途上のようです。
- implブロックの特殊化機能の提案: 1210-impl-specialization - The Rust RFC Book
- 特殊化機能のトラッキングissue: Tracking issue for specialization (RFC 1210)
- 健全な最小限の特殊化機能の実装: Implement a feature for a sound specialization subset
安定版のRustでは以下のように書く必要があり、VecとHashMapそれぞれに対してimplブロックを記述する必要があることが見て分かると思います。
use std::collections::HashMap;
use std::hash::Hash;
trait Mask {
fn mask(&self) -> Self;
}
impl Mask for String {
fn mask(&self) -> Self {
"*".repeat(self.len())
}
}
impl<T: Clone, U: Mask + Clone> Mask for (T, U) {
fn mask(&self) -> Self {
(self.0.clone(), self.1.mask())
}
}
impl<T: Mask + Clone> Mask for Vec<T> {
fn mask(&self) -> Self {
self.clone().into_iter().map(|x| x.mask()).collect::<Vec<T>>()
}
}
impl<T: Eq + Hash + Clone, U: Mask + Clone> Mask for HashMap<T, U> {
fn mask(&self) -> Self {
self.clone().into_iter().map(|x| x.mask()).collect::<HashMap<T, U>>()
}
}
// 以下のimplブロックはconflicting implementation(Stringへのimplブロックと衝突)でエラーに
// そのためIntoIteratorを持つクラスとしてVecやHashMapをまとめて扱うことができません
// 他に、HashMapを入れる場合はTにEq + Hashのトレイト境界も必要になるという問題も
// impl<T, U: Mask> Mask for T
// where T: Clone + std::iter::IntoIterator<Item=U> + std::iter::FromIterator<U>
// {
// fn mask(&self) -> T {
// self.clone().into_iter().map(|x| x.mask()).collect::<T>()
// }
// }
// Rustスタイルでは無いですが関数オーバーロードのように呼び出せる関数も定義できます
fn mask<T: Mask>(m: &T) -> T {
m.mask()
}
fn main() {
let s = String::from("foo");
let masked = s.mask(); // "***"
println!("{:?}", masked);
let v = vec![String::from("bar"), String::from("buzz")];
println!("{:?}", v.mask()); // ["***", "****"]
let mut map = HashMap::new();
map.insert(String::from("key1"), String::from("012-3456-7890"));
map.insert(String::from("key2"), String::from("123-4567-8901"));
println!("{:?}", map.mask()); // {"key1": "*************", "key2": "*************"}
let mut map_of_vec = HashMap::new();
map_of_vec.insert(String::from("person1"),
vec![String::from("012-3456-7890"), String::from("123-4567-8901")]);
println!("{:?}", map_of_vec.mask()); // {"person1": ["*************", "*************"]}
// 関数オーバーロードのように呼び出すことも可能
println!("{:?}", mask(&map_of_vec)); // {"person1": ["*************", "*************"]}
}
このように現状のRustではC++の関数オーバーロードに匹敵するほど柔軟な書き方はできません。
C++ではvector、map、unordered_map、setなどをiterableな型としてまとめて対応できましたが、RustではVecとHashMap個別にしか対応できておらず、BTreeMapやHashSetを追加する場合はそれぞれimplブロックを書き足す必要があります。
これはRustが特殊化をサポートしていないことに加え、implブロックでトレイト境界を厳しくチェックしていることも影響しているように思います。
ただ、C++は柔軟である一方で書き方を間違えた時のコンパイルエラーが膨大となり非常に難解だという欠点があります。
その点Rustは柔軟性を犠牲にしている代わりにコンパイルエラーが分かりやすく、ストレスがありません。
実際、HashMapのキーとなる型にはトレイト境界としてEqやHashが必要なのですが、これを書き忘れてコンパイルしても、コンパイルエラー文言がそのことを親切に教えてくれます。
今回の例でもRustとC++のスタンスの違いがよく表れていると感じました。
Rustに可変長引数関数の機能は無い
Rustには可変長引数の課題もあります。
C++ではvariadic templates機能によって任意の型の任意個の引数を持つ関数オーバーロードを定義できますが、Rustにその機能はありません。
template <typename T>
void println(const T& arg) {
std::cout << arg << std::endl;
}
template <typename T, typename... Args>
void println(const T& arg, const Args&... args) {
std::cout << arg << ", ";
println(args...);
}
int main() {
println("foo", 2, -1.1); // foo, 2, -1.1
}
そのためRustで可変長引数関数を実現するには再帰的なジェネリクスで実装するか、マクロを使うしかありません。
再帰的なジェネリクスを用いた可変長引数については以下の記事が参考になります。
-> Rustの可変長引数関数とHListの話
####無理やりRustで関数オーバーロードを書けるようにする試みもあるが……
"Rust function overloading"で検索すると、何とかしてRustで関数オーバーロードの記法を使えるようにしようという試みを見つけました。
たとえばoverloadfというクレートがあります。(ただし利用数は極めて少なくマイナー)
また、Rustで関数オーバーロードを実装したというブログもありますが、筆者自身がなぜこれを使ってはならないかを説いている程度にはトリッキーな実装となっています。
既に説明した通り、Rustでは少なくとも関数オーバーロード記法を使うのではなくて(特に長期にメンテナンスする必要のあるコードでは)トレイトを使うようにした方が良いのは確実といって良いでしょう。
まとめ
Rustにはトレイトがあるので関数オーバーロードを頑張る必要は無いということが分かりました。
C++のテンプレートメタプログラミングを活用した関数オーバーロード機能の柔軟性に比べるとRustのトレイトは貧弱ですが、C++でそういう関数オーバーロードを使いまくってた人以外は気にする必要のないレベルです。
少しでもそのような柔軟性に近づけそうなRustのimplブロックの特殊化機能については、多くの課題はあるようですが、今後の進展を期待したいです。