【问题标题】:Templates polymorphism模板多态性
【发布时间】:2010-02-04 21:34:45
【问题描述】:

我有这种类结构。

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

怎么了?模板不是多态的吗?

【问题讨论】:

标签: c++ templates polymorphism


【解决方案1】:

我认为您需要的确切术语是“模板协方差”,这意味着如果 B 继承自 A,那么 T&lt;B&gt; 会以某种方式继承自 T&lt;A&gt;。在 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&lt;A&gt;T&lt;B&gt; 视为完全不相关的类型。

那么您如何解决您面临的问题?在 Java 和 C# 中,您可以分别使用有界通配符约束

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

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

下一个 C++ 标准(称为 C++1x(以前称为 C++0x))最初包含一个更强大的机制,名为 Concepts,它可以让开发人员对模板参数强制执行语法和/或语义要求,但不幸的是被推迟到以后的日期。但是,Boost 有一个您可能感兴趣的Concept Check library

尽管如此,对于您遇到的问题,概念可能有点矫枉过正,使用@gf 提出的简单静态断言可能是最好的解决方案。

* 更新:从 .Net Framework 4 开始,可以将泛型参数标记为covariant or contravariant

【讨论】:

    【解决方案2】:

    这里有两个问题:默认结构的形式是MyClass c;;带括号的它看起来像编译器的函数声明。

    另一个问题是 Container&lt;Interface&gt; 只是与 Container&lt;Foo&gt; 不同的类型 - 您可以执行以下操作来实际获得多态性:

    Bar::Bar(const Container<Interface*>&) {}
    
    Container<Interface*> container;
    container.push_back(new Foo);
    Bar* temp = new Bar(container);
    

    当然,您也可以将 Bar 或其构造函数作为 Kornel 所示的模板。

    如果你真的想要一些类型安全的编译时多态性,你可以使用 Boost.TypeTraits is_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
    }
    

    【讨论】:

    • 这真的很好,正是我需要的。我不必更改很多已经实现的代码。再次感谢。
    • 我会使用 BOOST_MPL_ASSERT_MSG 而不是 BOOST_STATIC_ASSERT:BOOST_MPL_ASSERT_MSG( (boost::is_base_of&lt;Interface, T&gt;::value), T_MUST_BE_A_SUBCLASS_OF_INTERFACE___INSTEAD_T_IS, (T) ); 如果你使用 Foo2 而不是 Foo,错误应该是这样的:error: no matching function for call to ‘assertion_failed(mpl_::failed************ (T_MUST_BE_A_SUBCLASS_OF_INTERFACE___INSTEAD_T_IS::************)(Foo2))’
    • 感谢 GMan - 我需要对自然语言进行语法检查。
    • 错误 - 第 1 行:所有格后跟名词后跟所有格 - 预期的解释性陈述。
    • 太好了,我在哪里可以下载? ;)
    【解决方案3】:

    没有。想象一下,容器参数被“硬编码”到它定义的类中(这实际上就是它的工作方式)。因此容器类型为Container_Foo,与Container_Interface不兼容。

    但是你可以尝试的是:

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

    然而你以这种方式失去了直接类型检查。

    实际上,STL 方式(可能更有效和通用)是这样做的

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

    ...但我假设您没有在容器中实现迭代器。

    【讨论】:

    • 这很可悲。谢谢你的建议。我不喜欢那个解决方案,但恐怕它是唯一剩下的了。
    • 你猜对了。我不需要他们以那种特殊的方式。坦率地说,不知道如何实现它们,现在也没有时间学习。
    【解决方案4】:

    可以为容器创建继承树,反映数据的继承树。如果您有以下数据:

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

    这不是性能最好的代码;这只是给出一个想法。这种方法的唯一问题是每次添加新的 Data 类时,还必须创建一个新的 Container。除此之外,您还有“再次工作”的多态性。您可以具体或笼统:

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

    【讨论】:

      【解决方案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 类),则相当有效

      缺点:

      • 需要创建一个新容器,而不是启用原始容器的重用
      • 意味着创建和填充新容器需要一些内存和处理器开销,这在很大程度上取决于复制构造函数的成本

      【讨论】:

        【解决方案6】:
        #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;
        }
        
        

        【讨论】:

        • 请为您的回答提供上下文。
        【解决方案7】:

        container 是 Foo 对象的容器,而不是 Interface 对象的容器

        它也不能是多态的,指向事物的指针可以是,但不是对象本身。如果您可以将任何从接口派生的东西放入容器中,容器中的插槽必须有多大

        你需要

         container<Interface*>
        

        或更好

         container<shared_ptr<Interface> >
        

        【讨论】:

        • @pm100, shared_ptr?你用过shared_ptr吗??
        • @msiemeri - true,但请注意,答案的其余部分假定容器 存储值,而它实际上可能存储 Interface*
        • 一直是的,已更正,我可以否决反对票 - :-)
        • @pm100, container 可能有一个成员字段vector,在这种情况下你的评论不正确。
        • @kornel - 我同意,容器实际上可能存储 T*,甚至是 shared_ptr。如果是这样,那么我的第二段是多余的。未提供 Sicnce 代码我假设它已存储 T
        猜你喜欢
        • 2021-02-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-07-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-13
        相关资源
        最近更新 更多