web-dev-qa-db-ja.com

C ++テンプレートのポリモーフィズム

私はこのクラスの構造を持っています。

class Interface{
...
}

class Foo : public Interface{
...
}

template <class T>
class Container{
...
}

そして、私は他のクラスBarのこのコンストラクターを持っています。

Bar(const Container<Interface> & bar){
...
}

この方法でコンストラクターを呼び出すと、「一致する関数がありません」というエラーが発生します。

Container<Foo> container ();

Bar * temp = new Bar(container);

なにが問題ですか?テンプレートは多形ではありませんか?

29
Rusty Horse

必要なものの正確な用語は「テンプレート共分散」だと思います。つまり、BがAから継承する場合、どういうわけかT<B>T<A>から継承します。これはC++には当てはまらず、JavaおよびC#ジェネリック*にも当てはまりません。

テンプレートの共分散を回避するのには十分な理由があります。これにより、テンプレートクラスのすべての型安全性が削除されます。次の例で説明しましょう。

//Assume the following class hierarchy
class Fruit {...};

class Apple : public Fruit {...};

class Orange : public Fruit {...};

//Now I will use these types to instantiate a class template, namely std::vector
int main()
{
    std::vector<Apple> Apple_vec;
    Apple_vec.Push_back(Apple()); //no problem here

    //If templates were covariant, the following would be legal
    std::vector<Fruit> & fruit_vec = Apple_vec;

    //Push_back would expect a Fruit, so I could pass it an Orange
    fruit_vec.Push_back(Orange()); 

    //Oh no! I just added an orange in my Apple basket!
}

したがって、AとBの関係に関係なく、T<A>T<B>は完全に無関係なタイプと見なす必要があります。

では、直面している問題をどのように解決できますか? JavaおよびC#では、それぞれ制限付きワイルドカードおよびconstraints

//Java code
Bar(Container<? extends Interface) {...}

//C# code
Bar<T>(Container<T> container) where T : Interface {...}

次のC++標準(C++ 1x(以前のC++ 0x)として知られている)には、最初は Concepts という名前のさらに強力なメカニズムが含まれていました。これにより、開発者はテンプレートに構文要件や意味要件を適用できます。パラメータが、残念ながら後日延期されました。ただし、Boostには コンセプトチェックライブラリ があります。

それでも、概念は遭遇する問題に対して少しやり過ぎかもしれません。 @ gf によって提案された単純な静的アサーションを使用することがおそらく最良の解決策です。

*更新:.Net Framework 4以降、ジェネリックパラメーターが 共変または反変 であるとマークすることが可能です。

39
Luc Touraille

ここには2つの問題があります。デフォルトの構造はMyClass c;の形式です。括弧を付けると、コンパイラーにとっては関数宣言のように見えます。

もう1つの問題は、Container<Interface>が単にContainer<Foo>とは異なるタイプであるということです。代わりに、実際にポリモーフィズムを取得するために次のことを行うことができます。

Bar::Bar(const Container<Interface*>&) {}

Container<Interface*> container;
container.Push_back(new Foo);
Bar* temp = new Bar(container);

またはもちろん、Kornelが示したように、Barまたはそのコンストラクターをテンプレートにすることもできます。

型に安全なコンパイル時のポリモーフィズムが実際に必要な場合は、Boost.TypeTraitsis_base_of または同等のものを使用できます。

template<class T>
Bar::Bar(const Container<T>& c) {
    BOOST_STATIC_ASSERT((boost::is_base_of<Interface, T>::value));
    // ... will give a compile time error if T doesn't 
    // inherit from Interface
}
11
Georg Fritzsche

いいえ。コンテナパラメータが、それが定義するクラスに「ハードコード」されていると想像してください(実際にはそのように機能します)。したがって、コンテナタイプはContainer_Fooであり、Container_Interfaceとは互換性がありません。

しかし、あなたが試みるかもしれないことはこれです:

template<class T>
Bar(const Container<T> & bar){
...
}

しかし、あなたはそのように直接型チェックを失います。

実際には、STLの方法(おそらくより効果的で一般的)は

template<class InputIterator>
Bar(InputIterator begin, InputIterator end){
...
}

...しかし、コンテナにイテレータが実装されていないと思います。

5

テンプレート関数を使用する次の回避策を提案します。この例ではQtのQListを使用していますが、ソリューションが他のコンテナーに直接転置されることを妨げるものは何もありません。

template <class D, class B> // D (Derived) inherits from B (Base)
QList<B> toBaseList(QList<D> derivedList)
{
    QList<B> baseList;
    for (int i = 0; i < derivedList.size(); ++i) {
        baseList.append(derivedList[i]);
    }
    return baseList;
}

長所:

  • 一般
  • タイプセーフ
  • アイテムがポインタまたは他の安価にコピー構築可能な要素(暗黙的に共有されたQtクラスなど)である場合はかなり効率的です

短所:

  • 元のコンテナの再利用を可能にするのではなく、新しいコンテナを作成する必要があります
  • コピーコンストラクタのコストに大きく依存する、新しいコンテナの作成と入力の両方にいくらかのメモリとプロセッサのオーバーヘッドを意味します
2
Adriano Mitre

データの継承ツリーを反映して、コンテナの継承ツリーを作成することができます。次のデータがある場合:

class Interface {
public:
    virtual ~Interface()
        {}
    virtual void print() = 0;
};

class Number : public Interface {
public:
    Number(int value) : x( value )
        {}
    int get() const
        { return x; }
    void print()
        { std::printf( "%d\n", get() ); };
private:
    int x;
};

class String : public Interface {
public:
    String(const std::string & value) : x( value )
        {}
    const std::string &get() const
        { return x; }
    void print()
        { std::printf( "%s\n", get().c_str() ); }
private:
    std::string x;
};

次のコンテナを使用することもできます。

class GenericContainer {
public:
    GenericContainer()
        {}
    ~GenericContainer()
        { v.clear(); }

    virtual void add(Interface &obj)
        { v.Push_back( &obj ); }
    Interface &get(unsigned int i)
        { return *v[ i ]; }
    unsigned int size() const
        { return v.size(); }
private:
    std::vector<Interface *> v;
};

class NumericContainer : public GenericContainer {
public:
    virtual void add(Number &obj)
        { GenericContainer::add( obj ); }
    Number &get(unsigned int i)
        { return (Number &) GenericContainer::get( i ); }
};

class TextContainer : public GenericContainer {
public:
    virtual void add(String &obj)
        { GenericContainer::add( obj ); }
    String &get(unsigned int i)
        { return (String &) GenericContainer::get( i ); }
};

これは最高のパフォーマンスのコードではありません。それはただアイデアを与えることです。このアプローチの唯一の問題は、新しいデータクラスを追加するたびに、新しいコンテナも作成する必要があることです。それとは別に、あなたは「再び働く」多型を持っています。具体的または一般的である可能性があります。

void print(GenericContainer & x)
{
    for(unsigned int i = 0; i < x.size(); ++i) {
        x.get( i ).print();
    }
}

void printNumbers(NumericContainer & x)
{
    for(unsigned int i = 0; i < x.size(); ++i) {
        printf( "Number: " );
        x.get( i ).print();
    }
}

int main()
{
    TextContainer strContainer;
    NumericContainer numContainer;
    Number n( 345 );
    String s( "Hello" );

    numContainer.add( n );
    strContainer.add( s );

    print( strContainer );
    print( numContainer );
    printNumbers( numContainer );
}
2
Baltasarq
#include <iostream>
#include <sstream>
#include <map>
#include <vector>

struct Base { int b = 111; };
struct Derived: public Base { };

struct ObjectStringizer {
    template <typename T>
    static std::string to_string(const T& t) {
        return helper<T>()(t);
    }

    template <typename T, typename = void>
    struct helper {
        std::string operator()(const T& t) {
            std::ostringstream oss;
            oss << t;
            return oss.str();
        }
    };

    template <typename T>
    struct helper<T, typename std::enable_if<std::is_base_of<Base, T>::value>::type> {
        std::string operator()(const T& base) {
            return to_string(base.b);
        }
    };

    template <typename T>
    struct helper<std::vector<T>> {
        std::string operator()(const std::vector<T>& v) {
            std::ostringstream oss;
            for (size_t i = 0, sz = v.size(); i < sz; ++i) {
                oss << (i ? "," : "") << to_string(v[i]);
            }
            return "[" + oss.str() + "]";
        }
    };

    template <typename Key, typename Value>
    struct helper<std::map<Key, Value>> {
        std::string operator()(const std::map<Key, Value>& m) {
            std::ostringstream oss;
            for (auto iter = m.begin(), iter_end = m.end(); iter_end != iter; ++iter) {
                oss << (m.begin() != iter ? "," : "") << to_string(iter->first) << ":" << to_string(iter->second);
            }
            return "{" + oss.str() + "}";
        }
    };
};

int main(int argc, char* argv[]) {
    std::cout << ObjectStringizer::to_string("hello ") << ObjectStringizer::to_string(std::string("world")) << std::endl;
    std::cout << ObjectStringizer::to_string(Derived()) << std::endl;
    std::cout << ObjectStringizer::to_string(std::vector<int>{3, 5, 7, 9}) << std::endl;
    std::cout << ObjectStringizer::to_string(std::map<int, std::string>{{1, "one"}, {2, "two"}}) << std::endl;
    return 0;
}

0
user3015856