C++模板用于多态性?“使用类模板需要模板参数”

3

我刚开始学习C++,正在尝试使用模板来构建特定类型类的层次结构,以实现多态。我知道在Java中使用泛型可以很容易地做到这一点。我相信这个问题已经被问过了,但我不知道用什么术语在C++中搜索我想要的。

我想使用一个基类,该基类由下面定义的非常具体的Derived_1Derived_2类的对象组合而成,每个类都扩展自名为Base的类。然而,我得到了一个编译器错误,说我需要声明类型?我该使用什么语法来指示这个在实现中使用的z向量,可以由任何扩展自Base的类的任意组合组成?

以下是我目前的代码,我认为在声明基类和扩展该基类的类方面大部分是正确的:

base_types.h

template <typename T>
class Base {
public:
    Base<T>(size_t a, T b) :
            m_a(a),
            m_b(b) {
    }

    T getB() const;

    size_t m_a;
    T m_b;
};

// specific kind of Base that uses bool
class Derived_1 : public Base<bool> {
    Derived_1(uint32_t a);     // second parameter is unused, is assumed to be "true"
    bool getB();
};


// specific kind of Base that uses size_t
class Derived_2 : public Base<size_t> {
    Derived_2(uint32_t a, size_t b);
    size_t getB();
};

base_types.cpp

Derived_1::Derived_1(uint32_t a) : Base(a, true) { }      // second parameter is unused, is assumed to be "true"

bool Derived_1::getB() { return m_b; }


Derived_2::Derived_2(uint32_t a, size_t b) : Base(a, b) { }

bool Derived_2::getB() { return m_b; }

impl_types.h

#include "base_types.h"
#include <vector>

class Foo {
public:
    Foo(
        size_t y,
        const std::vector<Base>& z);      // Error: Use of class template 'Base' requires template arguments
};

impl_types.cpp

class Foo {
public:
    Foo(
        size_t y,
        const std::vector<Base>& z) :      // Error: Use of class template 'Base' requires template arguments
            m_y{y},
            m_z{z};
};

// Base.java
class Base<T> {
    int m_a;
    T m_b;

    Base(int a, T b) {
        m_a = a;
        m_b = b;
    }

    T getB() {
        return m_b;
    }
}

// Derived_1.java
class Derived_1 extends Base<Boolean> {
    Derived_1(int a, Boolean a) {
        super(a, b);
    }

    Boolean getB() {
        return m_b;
    }
}

// Derived_2.java
class Derived_2 extends Base<String> {
    Derived_2(int a, String b) {
        super(a, b);
    }

    String getB() {
        return m_b;
    }
}

// Main.java
public class Main {

    public static void main(String[] args) {
        List<Base> baseList = new ArrayList<>();
        baseList.add(new Derived_1(1, true));
        baseList.add(new Derived_2(2, "foo"));

        for(Base o : baseList) {
            System.out.println(o.getB());
        }
    }
}

2
请注意,Java的泛型与模板非常不同。虽然泛型使用某种类型擦除,但模板全部在编译时完成。在运行时没有模板,其中一个后果是您无法拥有类似于Java的new ArrayList<>()的等效物,除非您实现一些类似于Java泛型的类型擦除。 - 463035818_is_not_a_number
请在问题中包含错误消息。 - 463035818_is_not_a_number
@formerlyknownas_463035818,这个错误信息已经在标题和问题中提到了:“错误:使用类模板'...'需要模板参数”。 - Kyle Falconer
啊,好的,这在注释里面。我习惯忽略注释;)错误就是它所说的那样。在std::vector<T>中,T必须是一种类型,但是Base只是一个模板。 - 463035818_is_not_a_number
你到底想要实现什么?你能写一个C++的main函数并举个例子说明你想如何使用它吗?我很难从Java的例子中看出你在C++中想做什么,因为正如我所说的,new ArrayList<>();使用了泛型,而模板不是泛型。如果你想要一个可以容纳不同类的实例,这些类都继承自一个共同的基类的数组,那么有一个解决方案,但它不需要你编写一个模板。 - 463035818_is_not_a_number
@formerlyknownas_463035818 如果这不是实现的方法,我可以不使用模板。我只想要一个对象向量,其中包含从共同基类继承的不同类的对象,但是这些类之间共享一个方法,该方法根据特定类返回不同类型 - Kyle Falconer
4个回答

3
您不能使用std::vector<Base>,因为Base是模板。您应该使用std::vector<Base<bool>>std::vector<Base<size_t>>或类似的类型。Base<bool>Base<size_t>是不同的类型,除了使用相同的模板Base之外,没有其他共同点。
也许您想要的是std::vector<std::variant<Base<bool>,Base<size_t>>>,但很难从您的代码中确定。 std::variant用于具有可能没有任何共同点的不同类型值的变量。
您不能将派生类的对象放入基类的向量中,因此当您希望它们在同一个向量中按值使用时,应使用std::vector<std::variant<Derived1,Derived2>>。动态多态对象必须具有完全相同的基类,即使如此,您也需要通过引用而不是按值将这些对象放入容器中。

我需要一个可以是未知类型的东西。我事先不知道这个向量将持有什么样的“Base”,例如:std::vector<Base<?> > - Kyle Falconer
@KyleFalconer vector 存储其对象是连续的,因此 1)所有对象必须相同(大小相同)2)它们应该静态地知道。std::variant 提供了实现静态多态性的方法。在 Java 中,只有指针而没有值,因此最接近的类比就是 std::vector<Base*>,其中包含真正的 Base 类和虚拟方法。也就是说,您可以获得相同的结果,但不能使用模板,因为 Base<int>Base<bool> 是不同的“无关”类型。 - Dan M.

3

在Java中,这样做是可行的,因为ArrayList<Widget>基本上是ArrayList<Object>。这带来了一些优点,但也有一些缺点。

在C++中,使用模板代替泛型。虽然目标相似,但它们在实现和影响方面非常不同。

正如它们的名称所述:它们是模板。类模板不是类,而是一种只在编译时存在的实体。只有当实例化模板时,它才变成具体的东西。

ELI5关于实例化模板的过程: std::vector是模板。 std::vector<int>是模板的一个实例。通过填充代码中由模板参数表示的空白部分来实例化模板:

template<typename T>
auto value() -> T { return T{}; }

int main() {
    return value<int>() + value<short>();
}

有两个函数被实例化并且看起来像这样:

template<> //   v---- T has been replaced!
auto value() -> int { return int{}; }

template<> // Another instantiation
auto value() -> short { return short{}; }

当然,编译器并不是文本替换,而是使用AST来解析实际类型。
这是为了向您展示模板在处理后实际上不存在。只有实例化是具体的。而且还有多个实例化。
就像上面的函数模板一样,当模板类被实例化时,会创建一个全新的类型。实例化所创建的类型是完全不相关的。它们只是不同的类型,就像模板函数产生不同的实例化一样。
那么……如果你有许多不同、不相关的类,如何进行多态性呢?
添加一个接口!
struct Interface {
    // TODO: put useful function there
};

template<typename T>
struct Base : Interface {
    virtual auto getB() const -> T;
};

struct Impl1 : Base<bool> {
    auto getB() const -> T override;
};

如果相反,你的意图是像这样做:
Base b = ...;

// pseudocode
if (typeid(b->getB()) == bool)
{
    bool b = dynamic_cast<bool>(b->getB());
}
else if (typeid(b->getB()) == std::size_t)
{
    std::size_t b = dynamic_cast<std::size_t>(b->getB());
}

然后,像其他答案所述的那样,变体是一种解决方案。如果您像这样列出可能的类型,则可以预先知道 Base 可能的类型列表。因此,std::variant 就是你想要的。

为了更容易地使用 variant,您可以随时设置类型别名:

using VBase = std::variant<Base<bool>, Base<std::size_t>, ...>

如果你需要使用模板做多态,可以参考我之前在这个主题上的回答:混合模板和多态

2
如果您创建一个非模板的空基类,继承它的模板,然后是特化,您可以在向量或任何其他结构中存储基本指针。如果您想使用存储的对象,您必须显式地将它们转换回实际类型(如果我没记错的话,在Java中也必须这样做)。"最初的回答"

https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering - Balázs Kovacsics

2
错误:使用类模板Base需要模板参数
您会收到此错误,因为Base不是一种类型,而只是一个模板。不要将C++模板与Java泛型混淆,它们是非常不同的概念。您不能拥有一个包含模板的向量,因为模板只是模板。您需要实例化它们以获得类型。例如,您可以拥有一个std :: vector >。
您代码中的另一个问题是,Base应该具有虚析构函数。否则,您可能会遇到内存泄漏的危险。必须将方法声明为virtual以启用动态调度。
话虽如此...
您实际想做的事情:
我只想保存从共同基类继承的不同类的对象的向量,[...]
您不需要模板。那就像这样简单:
#include <iostream>
#include <vector>
#include <memory>
#include <utility>
struct base { 
    virtual void some_method(){ std::cout << "base\n";}
    virtual ~base(){}
};
struct foo : base {
    virtual void some_method() override { std::cout << "foo\n";}
};
struct bar : base {
    virtual void some_method() override { std::cout << "bar\n";}
};


int main() {
    std::vector<std::shared_ptr<base>> v;
    v.emplace_back(new foo());
    v.emplace_back(new bar());

    for (auto& e : v) e->some_method();
    return 0;
}

多态可以与指针或引用一起使用。由于在容器中存储引用不是那么直接,因此我使用了指针。我还使用了智能指针,因为我不想混乱手动内存管理。
到目前为止还好...
但是,对于一个在这些类之间共享的方法,它返回一个不同的类型,具体取决于该特定类,这并不容易。
首先要注意的是,同一方法不能具有不同的返回类型。举个例子,如果你有:
struct example_base { 
    virtual int foo() { return 1;} 
};
struct example_derived {
    virtual double foo() override { return 1.4; }
};

那么example_derived::foo不会覆盖example_base::foo!由于使用了override,编译器会在类似以下的错误信息中告知您:
prog.cc:20:24: error: 'foo' marked 'override' but does not override any member functions
        virtual double foo() override { return 1.4; }
                       ^

根据您实际想要实现的目标(为什么需要派生类与不同返回类型的“共享公共方法”?),有不同的解决方法。我将向您展示一种方法。
#include <iostream>
#include <vector>
#include <memory>
#include <utility>

struct return_type_base { 
    virtual void print() {}
    virtual ~return_type_base() {}
};

struct bool_return_type : return_type_base {
    bool value = true;
    virtual void print() { std::cout << value << "\n"; }
};
struct int_return_type : return_type_base {
    int value = 3;
    virtual void print() { std::cout << value << "\n"; }
};

using return_type_ptr = std::shared_ptr<return_type_base>;

struct base { 
    virtual return_type_ptr some_method() = 0;
    virtual ~base(){}
};
struct foo : base {
    virtual return_type_ptr some_method() override { 
        return return_type_ptr(new bool_return_type());
    }
};
struct bar : base {
    virtual std::shared_ptr<return_type_base> some_method() override { 
        return return_type_ptr(new int_return_type());
    }
};


int main() {
    std::vector<std::shared_ptr<base>> v;
    v.emplace_back(new foo());
    v.emplace_back(new bar());

    for (auto& e : v) e->some_method()->print();
    return 0;
}

这基本上是与上面完全相同的方法。为了以多态方式处理不同类型,我们声明一个共同的基类,并在该基类的共享指针上工作。正如之前提到的那样,这只是一种可能的方法。请带着一点怀疑看待它,它只是旨在给你一个起点。其主要缺点是:它是侵入性的(你必须为每个想返回的类型编写一个类),并且它使用虚函数(即运行时开销)。有更好的方法,但具体细节取决于您实际想要对这些不同的返回类型做什么。为了进一步阅读,建议您搜索“类型擦除”。
简而言之,模板是纯粹的编译时概念。如果你想在运行时平等地处理不同的类型,你需要某种形式的运行时类型擦除。在Java和C++中可用的技术相当不同。通过虚函数的运行时多态在两者中都可以工作,并且可能是最易于理解的方法,特别是当你来自Java时。你还应该看看标准库提供了什么(std::any, std::variant)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接