web-dev-qa-db-ja.com

クラスメンバー関数テンプレートは仮想的にできますか?

C++クラスメンバー関数テンプレートは仮想的にはできないと聞いたことがあります。これは本当ですか?

それらが仮想的である可能性がある場合、そのような機能を使用するシナリオの例は何ですか?

276
WannaBeGeek

テンプレートは、コンパイル時にコードを生成するコンパイラに関するものです。仮想関数は、実行時に実行時にどの関数を呼び出すかを考え出すということです。

実行時システムは、テンプレート化された仮想関数を呼び出す必要があると判断したら、コンパイルはすべて完了し、コンパイラは適切なインスタンスを生成できなくなります。したがって、仮想メンバー関数テンプレートを持つことはできません。

しかし、ポリモーフィズムとテンプレートを組み合わせることから生じる、いくつかの強力で興味深い技術、特にいわゆるtype erasureがあります。

298
sbi

C++テンプレートからThe Complete Guide:

メンバー関数テンプレートは仮想宣言できません。仮想関数呼び出しメカニズムの通常の実装では、仮想関数ごとに1つのエントリを持つ固定サイズのテーブルを使用するため、この制約があります。ただし、メンバー関数テンプレートのインスタンス化の数は、プログラム全体が翻訳されるまで固定されません。したがって、仮想メンバ関数テンプレートをサポートするには、C++コンパイラおよびリンカでまったく新しい種類のメカニズムをサポートする必要があります。対照的に、クラステンプレートの通常のメンバは、クラスがインスタンス化されるときにその数が固定されるため、仮想的になる可能性があります。

111
InQusitive

C++では、現時点では仮想テンプレートメンバー関数を許可していません。最も可能性の高い理由は、それを実装することの複雑さです。 Rajendraはそれが今できない理由を正当に示していますが、それは標準の合理的な変更で可能であるかもしれません。特に仮想関数呼び出しの場所を考えると、テンプレート関数のインスタンス化が実際にいくつ存在するかを調べてvtableを構築するのは難しいようです。標準の人々は今やらなければならないことが他にもたくさんあります。C++ 1xはコンパイラを書く人たちにとっても多くの仕事です。

いつテンプレート化されたメンバ関数が必要になりますか?私はかつて階層を純粋な仮想基底クラスでリファクタリングしようとするような状況に遭遇しました。それは異なる戦略を実行するための貧弱なスタイルでした。仮想関数の1つの引数を数値型に変更し、メンバー関数をオーバーロードする代わりに、すべてのサブクラスですべてのオーバーロードをオーバーライドすることを望みました。仮想テンプレート関数を使用しようとしました。 。)

32
pmr

仮想機能テーブル

仮想関数テーブルとそれらがどのように機能するかについてのいくつかの背景から始めましょう( source ):

[20.3]仮想メンバ関数と非仮想メンバ関数の呼び出し方法の違いは何ですか?

非仮想メンバー関数は静的に解決されます。つまり、メンバー関数は、オブジェクトへのポインタ(または参照)の型に基づいて静的に(コンパイル時に)選択されます。

対照的に、仮想メンバー関数は動的に(実行時に)解決されます。つまり、メンバー関数は、そのオブジェクトへのポインター/参照のタイプではなく、オブジェクトのタイプに基づいて(実行時に)動的に選択されます。これは「動的バインディング」と呼ばれます。ほとんどのコンパイラは、次のようなテクニックを使用しています。オブジェクトに1つ以上の仮想関数がある場合、コンパイラはオブジェクトに隠しポインタを「仮想ポインタ」または「vポインタ」と呼びます。このvポインタは、 "virtual-table"または "v-table"と呼ばれるグローバルテーブルを指します。

コンパイラは、少なくとも1つの仮想関数を持つクラスごとにvテーブルを作成します。たとえば、Circleクラスにdraw()、move()、およびresize()の仮想関数がある場合は、たとえガジェットのCircleオブジェクトがあっても、クラスCircleに関連付けられたVテーブルは1つだけです。これらのCircleオブジェクトはそれぞれ、Circle vテーブルを指します。 vテーブル自体には、クラス内の各仮想関数へのポインタがあります。たとえば、Circle v-tableには、Circle :: draw()へのポインタ、Circle :: move()へのポインタ、およびCircle :: resize()へのポインタの3つのポインタがあります。

仮想関数のディスパッチ中に、ランタイムシステムはオブジェクトのvポインタをクラスのvテーブルに追従させ、次にvテーブル内の適切なスロットをメソッドコードに追従させます。

上記の手法によるスペースコストのオーバーヘッドはわずかです。オブジェクトごとの追加ポインタ(ただし動的バインディングを実行する必要があるオブジェクトの場合のみ)、およびメソッドごとの追加ポインタ(仮想メソッドの場合のみ)。時間とコストのオーバーヘッドもかなりわずかです。通常の関数呼び出しと比較して、仮想関数呼び出しは2回の追加フェッチ(1つはvポインタの値を取得するため、もう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;

私はそれがいたいのですが、それは仮想テンプレートコンボのためにコンパイルされません:

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は基本クラスで宣言されていないことに注意してください。


これは回避策を伴う別のスタックオーバーフローの答えです: 仮想テンプレートメンバーの回避策が必要です

16
Mark Essel

ウィンドウ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&)'

それは明らかです:

  • 仮想メンバー関数はクラステンプレートで使用できます。コンパイラがvtableを構築するのは簡単です
  • ご覧のとおり、クラステンプレートのメンバ関数を仮想関数として定義することは不可能です。関数のシグネチャを決定してvtableエントリを割り当てることは困難です。
12
Brent81

いいえ、できません。しかし:

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);
};

あなたがしたいのが共通のインターフェースを持ち、サブクラスへの実装を延期することであるならば、ほとんど同じ効果があります。

11
Tom

いいえ、テンプレートメンバー関数は仮想化できません。

5
dirkgently

質問の後半部分に答えるには:

それらが仮想的である可能性がある場合、そのような機能を使用するシナリオの例は何ですか?

これはやりたくない不合理なことではありません。たとえば、Java(すべてのメソッドが仮想の場合)では、汎用メソッドを使用しても問題はありません。

C++で仮想関数テンプレートを必要とする例としては、ジェネリックイテレータを受け入れるメンバ関数があります。汎用関数オブジェクトを受け取るメンバ関数です。

この問題を解決するには、boost :: any_rangeとboost :: functionと共に型消去を使うことで、関数をテンプレートにする必要なしに一般的なイテレータかファンクタを受け入れることができるようになる。

3
exclipy

テンプレートメソッドのタイプのセットが事前にわかっている場合は、「仮想テンプレートメソッド」の回避策があります。

わかりやすくするために、以下の例では2つのタイプ(intdouble)のみを使用しています。

そこでは、 'virtual'テンプレートメソッド(Base::Method)が対応する仮想メソッド(Base::VMethodのうちの1つ)を呼び出し、次にそれがテンプレートメソッド実装(Impl::TMethod)を呼び出します。

派生実装でテンプレートメソッドTMethodを実装し(AImplBImpl)、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は公開して直接使用することができます)。私はそれを追加したので、実際の「仮想」テンプレートメソッドのように見えます。

2
sad1raf

少なくとも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
0
Maxim Sinev

他の答えでは、提案されたテンプレート関数はファサードであり、実用上の利点はありません。

  • テンプレート関数は、異なる型を使って一度だけコードを書くのに役立ちます。
  • 仮想関数は、異なるクラスに共通のインターフェースを持つのに役立ちます。

この言語では仮想テンプレート機能は許可されていませんが、回避策として両方を使用することができます。クラスごとに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

お試しください ここ

0
andreaplanet