C++のVarianceの話、といってもstd::variant
のことではない。
共変リターンタイプ
struct Animal {
};
struct Cat : public Animal {
};
このようなクラスがあったときに
std::function<Animal* ()> func1 = []() { return new Cat;};
このコードは問題なくコンパイルが通る。func1()
はCat*
しか返さないがAnimal*
を返すという宣言に嘘はない。
このようにサブタイプ(C++でいうところの子クラス)を返す関数を許容する特徴を「共変リターンタイプ(covariant return types)」と呼ぶ。
反変パラメタタイプ
先程の「共変リターンタイプ」は返り値についてであった。では引数についてはどうだろうか。
std::function<void (Animal*)> func2 = [](Cat*) {}; // ERROR
返り値の場合と同様に引数にサブタイプを指定しているが、このコードはコンパイルが通らない。
std::function<void (Animal*)>
はAnimal
のサブタイプ(Dog
とかいるかも)をすべて受け入れる必要があるが、[](Cat*){}
はCat
としか受け入れることができないのでコンパイルが通ってしまっては不都合が生じることになる。
だが次のコードはコンパイルが通る
std::function<void (Cat*)> func3 = [](Animal*) {};
コンパイルが通らなかった例と比べると引数の型が逆になっている。
一見奇妙な感じを受けるが、Cat*
しか引数に取らない関数としてAnimal*
を引数に取る関数を渡したとして、その後func3()
にはCat*
しか渡されないだろうが、Cat*
もAnimal*
なのだから特に問題はないはずだ。
このようにリターンタイプのときとはちょうど許容する型が逆転しているように見えるこれを「反変パラメタタイプ(contravariant parameter types)」という。
以上のようにコンパイラは型の位置(引数、返り値)によって許容するかどうかの判定をおこなっている。
メソッドのVariance
いままではstd::function
について見てきたがメソッドについてはどうだろうか。
struct Breeder {
virtual Animal* product()=0;
};
struct CatBreeder : public Breeder {
Cat* product() override {
return new Cat;
}
};
これはコンパイルが通る。メソッドについても共変リターンタイプを持っていると言える。
しかし次はコンパイルは通らない
struct AnimalDoctor {
virtual void treat(Animal*) = 0;
};
struct CatDoctor : public AnimalDoctor {
virtual void treat(Cat*) override {};
};
反変的なものを期待した次のコードもコンパイルは通らない。
struct CatDoctor {
virtual void treat(Cat*) {};
};
struct AnimalDoctor : public CatDoctor {
void treat(Animal*) override {};
};
メソッドは引数について「不変(invariant)」である。
STLコンテナについては「不変」
以下のコードはコンパイルは通らない。
const std::vector<Animal*>* v = new std::vector<Cat*>{};
クラス以外のVariance
C++のVarianceはクラスに限った話ではない。例えばint*
はconst int*
のサブタイプとみなされ、今までの例と同様に以下のコードでコンパイルが通る。
std::function<const int* ()> func4 = []() -> int* { return new int(1);};
std::function<void (int*)> func5 = [](const int*) {};
同様に次のコードはコンパイルが通らない。
std::function<int* ()> func6 = []() -> const int* { return new int(1);};
std::function<void (const int*)> func7 = [](int*) {};
QA
Q. Varianceの知識ってプログラミングで必要?
A. C++で自分が使うのはメソッドの共変リターンタイプくらい。ほとんど必要ない。
Q. じゃあなぜこの記事を書いたのか?
A. 実はRustではVarianceの知識がそこそこ必要になるのでその入門としてC++ある程度知っている人向けに書いた。Varianceはクラスに限らないという話をしたが、Rustのライフタイムにもサブタイプの概念があり1、記述位置によって共変、反変が存在し(C++とよく似ている)コンパイルが通らなくなりユーザーを悩ませるのだ。
参考
- https://quuxplusone.github.io/blog/2019/01/20/covariance-and-contravariance/
- https://eli.thegreenplace.net/2018/covariance-and-contravariance-in-subtyping/
-
例えばライフタイム
'a
がライフタム'b
を包含するとき'a
は'b
のサブタイプとなる。 ↩