C ++クラスメンバー関数テンプレートは仮想化できないと聞いたことがあります。これは本当ですか?
それらが仮想である場合、そのような機能を使用するシナリオの例は何ですか?
C ++クラスメンバー関数テンプレートは仮想化できないと聞いたことがあります。これは本当ですか?
それらが仮想である場合、そのような機能を使用するシナリオの例は何ですか?
回答:
テンプレートは、コンパイル時にコードを生成するコンパイラに関するものです。仮想関数はすべて、実行時に呼び出す関数を判別するランタイムシステムに関するものです。
ランタイムシステムがテンプレート化された仮想関数を呼び出す必要があると判断すると、コンパイルはすべて完了し、コンパイラーは適切なインスタンスを生成できなくなります。したがって、仮想メンバー関数テンプレートを持つことはできません。
ただし、ポリモーフィズムとテンプレートの組み合わせから生じるいくつかの強力で興味深い手法、特にいわゆる型消去があります。
Virtual functions are all about the run-time system figuring out which function to call at run-time
-申し訳ありませんが、これはかなり間違った方法であり、かなり混乱しています。これは単なる間接的なものであり、「実行時の計算」は含まれません。コンパイル時に、呼び出される関数はvtableのn番目のポインターが指すものであることがわかっています。「考え出す」とは、型チェックなどがあることを意味しますが、そうではありません。Once the run-time system figured out it would need to call a templatized virtual function
-関数が仮想かどうかはコンパイル時にわかります。
void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }
呼び出された時点で呼び出された関数を「認識」しており、のcb.f()
ことを知りませんvb.f()
。後者は、実行時システムによって、実行時に検出される必要があります。これを「計算」と呼びたいかどうか、またこれが多かれ少なかれ効率的であるかどうかにかかわらず、これらの事実は少しも変わりません。
C ++テンプレートから完全ガイド:
メンバー関数テンプレートは仮想として宣言できません。仮想関数呼び出しメカニズムの通常の実装では、仮想関数ごとに1つのエントリを持つ固定サイズのテーブルを使用するため、この制約が課されます。ただし、メンバー関数テンプレートのインスタンス化の数は、プログラム全体が翻訳されるまで固定されません。したがって、仮想メンバー関数テンプレートをサポートするには、C ++コンパイラとリンカーでまったく新しい種類のメカニズムをサポートする必要があります。対照的に、クラステンプレートの通常のメンバーは、クラスがインスタンス化されるときにその数が固定されるため、仮想にすることができます
現在、C ++は仮想テンプレートメンバー関数を許可していません。最も可能性の高い理由は、実装の複雑さです。Rajendraは、現時点でそれができない理由を説明していますが、標準を合理的に変更することで可能になる可能性があります。特に、仮想関数呼び出しの場所を考慮すると、テンプレート関数のインスタンス化が実際にいくつ存在するかを調べ、vtableを構築することは難しいようです。標準の人々は今やるべきことが他にもたくさんあるだけであり、C ++ 1xはコンパイラー作成者にとっても多くの作業です。
テンプレート化されたメンバー関数が必要になるのはいつですか?私はかつて、純粋な仮想基本クラスで階層をリファクタリングしようとする状況に遭遇しました。これは、さまざまな戦略を実装するための貧弱なスタイルでした。仮想関数の1つの引数を数値型に変更し、メンバー関数をオーバーロードする代わりに、仮想テンプレート関数を使用しようとしたすべてのサブクラスのすべてのオーバーロードをオーバーライドしたかった(そして、それらが存在しないことを確認する必要があった) 。)
仮想関数テーブルの背景とその機能(ソース)から始めましょう。
[20.3]仮想メンバー関数と非仮想メンバー関数の呼び出し方法の違いは何ですか?
非仮想メンバー関数は静的に解決されます。つまり、メンバー関数は、オブジェクトへのポインター(または参照)の型に基づいて静的に(コンパイル時に)選択されます。
対照的に、仮想メンバー関数は動的に(実行時に)解決されます。つまり、メンバー関数は、そのオブジェクトへのポインター/参照のタイプではなく、オブジェクトのタイプに基づいて(実行時に)動的に選択されます。これは「動的バインディング」と呼ばれます。ほとんどのコンパイラーは、次の手法のバリアントを使用します。オブジェクトに1つ以上の仮想関数がある場合、コンパイラーは「仮想ポインター」または「vポインター」と呼ばれる隠しポインターをオブジェクトに配置します。このvポインターは、「仮想テーブル」または「vテーブル」と呼ばれるグローバルテーブルを指します。
コンパイラーは、少なくとも1つの仮想関数を持つクラスごとにvテーブルを作成します。たとえば、クラスCircleがdraw()およびmove()とresize()の仮想関数を持っている場合、gazillion Circleオブジェクトがあり、vポインタがこれらのCircleオブジェクトはそれぞれ、Circle vテーブルをポイントします。vテーブル自体には、クラス内の各仮想関数へのポインターがあります。たとえば、Circle vテーブルには3つのポインタがあります。Circle:: draw()へのポインタ、Circle :: move()へのポインタ、Circle :: resize()へのポインタです。
仮想関数のディスパッチ中、ランタイムシステムはオブジェクトのvポインターをクラスのvテーブルに移動し、次にvテーブルの適切なスロットをメソッドコードに移動します。
上記の手法のスペースコストのオーバーヘッドはわずかです。オブジェクトごとに追加のポインター(ただし、動的バインディングを実行する必要があるオブジェクトのみ)と、メソッドごとに追加のポインター(仮想メソッドのみ)があります。時間コストのオーバーヘッドもかなりわずかです。通常の関数呼び出しと比較して、仮想関数呼び出しには2つの追加のフェッチが必要です(1つはvポインターの値を取得するため、もう1つはメソッドのアドレスを取得するため)。コンパイラーはポインターのタイプに基づいてコンパイル時に排他的に非仮想関数を解決するため、このランタイム活動は非仮想関数では発生しません。
私はこのようなものを、さまざまなタイプのキューブ(一部はピクセルで保存され、一部は画像で保存されるなど)に異なる方法で実装されるテンプレート化された最適化されたロード関数を含むcubefile基本クラスに使用しようとしています。
いくつかのコード:
virtual void LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
私はそれを望んでいますが、仮想テンプレート化されたコンボのためにコンパイルされません:
template<class T>
virtual void LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
テンプレート宣言をクラスレベルに移動してしまいました。このソリューションでは、プログラムがそれらを読み取る前に読み取る特定のタイプのデータについて知る必要がありましたが、これは受け入れられません。
警告、これはあまりきれいではありませんが、反復実行コードを削除することができました
1)基本クラス
virtual void LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
2)と子クラス
void LoadCube(UtpBipCube<float> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
template<class T>
void LoadAnyCube(UtpBipCube<T> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1);
LoadAnyCubeは基本クラスで宣言されていないことに注意してください。
回避策を含む別のスタックオーバーフローの回答を次に示します 。仮想テンプレートメンバーの回避策が必要です。
次のコードは、Windows 7でMinGW G ++ 3.4.5を使用して、コンパイルして適切に実行できます。
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class A{
public:
virtual void func1(const T& p)
{
cout<<"A:"<<p<<endl;
}
};
template <typename T>
class B
: public A<T>
{
public:
virtual void func1(const T& p)
{
cout<<"A<--B:"<<p<<endl;
}
};
int main(int argc, char** argv)
{
A<string> a;
B<int> b;
B<string> c;
A<string>* p = &a;
p->func1("A<string> a");
p = dynamic_cast<A<string>*>(&c);
p->func1("B<string> c");
B<int>* q = &b;
q->func1(3);
}
そして出力は:
A:A<string> a
A<--B:B<string> c
A<--B:3
その後、新しいクラスXを追加しました。
class X
{
public:
template <typename T>
virtual void func2(const T& p)
{
cout<<"C:"<<p<<endl;
}
};
次のようにmain()でクラスXを使用しようとしたとき:
X x;
x.func2<string>("X x");
g ++は次のエラーを報告します。
vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'
したがって、次のことが明らかです。
いいえ、できません。だが:
template<typename T>
class Foo {
public:
template<typename P>
void f(const P& p) {
((T*)this)->f<P>(p);
}
};
class Bar : public Foo<Bar> {
public:
template<typename P>
void f(const P& p) {
std::cout << p << std::endl;
}
};
int main() {
Bar bar;
Bar *pbar = &bar;
pbar -> f(1);
Foo<Bar> *pfoo = &bar;
pfoo -> f(1);
};
共通のインターフェースを持ち、サブクラスへの実装を延期するだけの場合も、ほとんど同じ効果があります。
Foo
ようポインタは、資格のあるFoo<Bar>
、それが指すことができない、Foo<Barf>
またはFoo<XXX>
。
いいえ、テンプレートメンバー関数は仮想化できません。
他の回答では、提案されたテンプレート関数はファサードであり、実際的なメリットはありません。
言語は仮想テンプレート関数を許可していませんが、回避策を使用すると、両方の機能を持つことができます。たとえば、クラスごとに1つのテンプレート実装と仮想共通インターフェースがあります。
ただし、テンプレートタイプの組み合わせごとに、ダミーの仮想ラッパー関数を定義する必要があります。
#include <memory>
#include <iostream>
#include <iomanip>
//---------------------------------------------
// Abstract class with virtual functions
class Geometry {
public:
virtual void getArea(float &area) = 0;
virtual void getArea(long double &area) = 0;
};
//---------------------------------------------
// Square
class Square : public Geometry {
public:
float size {1};
// virtual wrapper functions call template function for square
virtual void getArea(float &area) { getAreaT(area); }
virtual void getArea(long double &area) { getAreaT(area); }
private:
// Template function for squares
template <typename T>
void getAreaT(T &area) {
area = static_cast<T>(size * size);
}
};
//---------------------------------------------
// Circle
class Circle : public Geometry {
public:
float radius {1};
// virtual wrapper functions call template function for circle
virtual void getArea(float &area) { getAreaT(area); }
virtual void getArea(long double &area) { getAreaT(area); }
private:
// Template function for Circles
template <typename T>
void getAreaT(T &area) {
area = static_cast<T>(radius * radius * 3.1415926535897932385L);
}
};
//---------------------------------------------
// Main
int main()
{
// get area of square using template based function T=float
std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
float areaSquare;
geometry->getArea(areaSquare);
// get area of circle using template based function T=long double
geometry = std::make_unique<Circle>();
long double areaCircle;
geometry->getArea(areaCircle);
std::cout << std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl;
return 0;
}
出力:
正方形の面積は1、円の面積は3.1415926535897932385
質問の2番目の部分に答えるには:
それらが仮想である場合、そのような機能を使用するシナリオの例は何ですか?
これは無理なことではありません。たとえば、Java(すべてのメソッドが仮想である)は、ジェネリックメソッドに問題はありません。
C ++で仮想関数テンプレートが必要な例の1つは、ジェネリックイテレータを受け入れるメンバー関数です。または、ジェネリック関数オブジェクトを受け入れるメンバー関数。
この問題の解決策は、型消去をboost :: any_rangeおよびboost :: functionで使用することです。これにより、関数をテンプレートにすることなく、汎用のイテレーターまたはファンクターを受け入れることができます。
テンプレートメソッドのタイプのセットが事前にわかっている場合は、「仮想テンプレートメソッド」の回避策があります。
アイデアを示すために、以下の例では2つのタイプのみが使用されています(int
およびdouble
)。
そこで、「仮想」テンプレートメソッド(Base::Method
)が対応する仮想メソッド(の1つBase::VMethod
)を呼び出し、次にそれがテンプレートメソッド実装(Impl::TMethod
)を呼び出します。
TMethod
派生実装(AImpl
、BImpl
)でテンプレートメソッドを実装して使用するだけで済みますDerived<*Impl>
。
class Base
{
public:
virtual ~Base()
{
}
template <typename T>
T Method(T t)
{
return VMethod(t);
}
private:
virtual int VMethod(int t) = 0;
virtual double VMethod(double t) = 0;
};
template <class Impl>
class Derived : public Impl
{
public:
template <class... TArgs>
Derived(TArgs&&... args)
: Impl(std::forward<TArgs>(args)...)
{
}
private:
int VMethod(int t) final
{
return Impl::TMethod(t);
}
double VMethod(double t) final
{
return Impl::TMethod(t);
}
};
class AImpl : public Base
{
protected:
AImpl(int p)
: i(p)
{
}
template <typename T>
T TMethod(T t)
{
return t - i;
}
private:
int i;
};
using A = Derived<AImpl>;
class BImpl : public Base
{
protected:
BImpl(int p)
: i(p)
{
}
template <typename T>
T TMethod(T t)
{
return t + i;
}
private:
int i;
};
using B = Derived<BImpl>;
int main(int argc, const char* argv[])
{
A a(1);
B b(1);
Base* base = nullptr;
base = &a;
std::cout << base->Method(1) << std::endl;
std::cout << base->Method(2.0) << std::endl;
base = &b;
std::cout << base->Method(1) << std::endl;
std::cout << base->Method(2.0) << std::endl;
}
出力:
0
1
2
3
注意:
Base::Method
実際のコードでは余剰です(VMethod
公開して直接使用できます)。実際の「仮想」テンプレートメソッドのように見えるように追加しました。
Base
これまでに実装されたものと互換性のない引数型を使用してテンプレート関数を呼び出す必要があるたびに元のクラスを変更する必要があるという事実を回避することはできません。テンプレートの目的は、この必要性を回避することです...
多くの人が答えてきた古い質問は、投稿された他の方法とそれほど変わらない簡潔な方法であると思いますが、マイナーマクロを使用してクラス宣言の複製を容易にすることです。
// abstract.h
// Simply define the types that each concrete class will use
#define IMPL_RENDER() \
void render(int a, char *b) override { render_internal<char>(a, b); } \
void render(int a, short *b) override { render_internal<short>(a, b); } \
// ...
class Renderable
{
public:
// Then, once for each on the abstract
virtual void render(int a, char *a) = 0;
virtual void render(int a, short *b) = 0;
// ...
};
さて、サブクラスを実装するには:
class Box : public Renderable
{
public:
IMPL_RENDER() // Builds the functions we want
private:
template<typename T>
void render_internal(int a, T *b); // One spot for our logic
};
ここでの利点は、新しくサポートされたタイプを追加するときに、すべて抽象ヘッダーから実行でき、複数のソース/ヘッダーファイルで修正することを控えることができるということです。
少なくともgcc 5.4では、仮想関数はテンプレートメンバーになることができますが、それ自体がテンプレートでなければなりません。
#include <iostream>
#include <string>
class first {
protected:
virtual std::string a1() { return "a1"; }
virtual std::string mixt() { return a1(); }
};
class last {
protected:
virtual std::string a2() { return "a2"; }
};
template<class T> class mix: first , T {
public:
virtual std::string mixt() override;
};
template<class T> std::string mix<T>::mixt() {
return a1()+" before "+T::a2();
}
class mix2: public mix<last> {
virtual std::string a1() override { return "mix"; }
};
int main() {
std::cout << mix2().mixt();
return 0;
}
アウトプット
mix before a2
Process finished with exit code 0
これを試して:
classeder.hに書き込みます。
template <typename T>
class Example{
public:
T c_value;
Example(){}
T Set(T variable)
{
return variable;
}
virtual Example VirtualFunc(Example paraM)
{
return paraM.Set(c_value);
}
これを使用している場合は、このコードをmain.cppに書き込むことを確認してください。
#include <iostream>
#include <classeder.h>
int main()
{
Example exmpl;
exmpl.c_value = "Hello, world!";
std::cout << exmpl.VirtualFunc(exmpl);
return 0;
}