【问题标题】:C++ Errors declaring Interface with return template使用返回模板声明接口的 C++ 错误
【发布时间】:2019-04-06 02:13:55
【问题描述】:

我有一个基本接口,声明如下 - IBaseTest.h:

#pragma once
template <class T1>
class IBaseTest
{
public:

    virtual ~IBaseTest();
    virtual T1 DoSomething() = 0;
};

还有两个覆盖 DoSomething() CBaseTest1 类的孩子 - BaseTest1.h:

#pragma once
#include "IBaseTest.h"
class CBaseTest1: public IBaseTest<int>
{
public:
    virtual int DoSomething();
};

BaseTest1.cpp:

#include "BaseTest1.h"

int CBaseTest1::DoSomething()
{
    return -1;
}

和 CBaseTest2 在 - BaseTest2.h

#pragma once
#include "IBaseTest.h"
class CBaseTest2: public IBaseTest<long long>
{
public:
    virtual long long DoSomething();
};

BaseTest2.cpp:

#include "BaseTest2.h"

long long CBaseTest2::DoSomething()
{
    return -2;
}

所以 CBaseTest1::DoSomething() 将返回类型覆盖为 int,并将 CBaseTest2::DoSomething() 覆盖为 long long。现在,我想使用指向基接口的指针来处理这些类,但我遇到了问题:

#include "IBaseTest.h"
#include "BaseTest1.h"
#include "BaseTest2.h"


int _tmain(int argc, _TCHAR* argv[])
{
    IBaseTest<T1> * pBase = NULL;

    pBase = new CBaseTest1();

    cout << pBase->DoSomething() << endl;

    pBase = new CBaseTest2();

    cout << pBase->DoSomething() << endl;

    getchar();

    return 0;
}

问题是我不能声明IBaseTest&lt;T1&gt; * pBase = NULL; T1 是未定义的。如果像这样在 _tmain 之前声明模板:

template <class T1>

int _tmain(int argc, _TCHAR* argv[])
{ 
  ...
}

我得到:error C2988: unrecognizable template declaration/definition

那么我在这里放什么而不是 T1?

IBaseTest&lt;??&gt; * pBase = NULL;

【问题讨论】:

  • CBaseTest1CBaseTest2 不共享一个共同的基本类型。没有一种通用类型可以让你的指针指向其中任何一个(void* 除外)。
  • 什么都没有。 IBaseTest&lt;int&gt;IBaseTest&lt;long long&gt; 是不同且完全不相关的类型。
  • 好的,所以不可能这样做
  • 你可以添加一个公共基础来多态地使用它们,尽管你想要不同的返回类型 CbaseTest1CbaseTest2 实际上几乎没有共同点
  • 由于您尝试做的事情无法直接完成,因此您最好告诉我们您尝试解决的问题,因为他们可能存在比您尝试做的更好的选择.

标签: c++ class


【解决方案1】:

问题是在实例化模板类IBaseTest的对象时需要知道T1参数。从技术上讲,IBaseTest&lt;int&gt;IBaseTest&lt;long long&gt; 是两种不同的类型,没有共同的基础,C++ 不允许您声明变量IBaseTest&lt;T1&gt; pBase = NULL;,其中T1 是在运行时确定的。您要实现的目标是在动态类型语言中可能实现的目标,但在 C++ 中则不然,因为它是静态类型的。

但是,如果您在调用该方法时知道DoSomething 的预期返回类型,则可以使您的示例正常工作。首先,您需要引入一个不是模板的通用基类:

#include <typeinfo>
#include <typeindex>
#include <assert.h>

class IDynamicBase {
public:
  virtual std::type_index type() const = 0;

  virtual void doSomethingVoid(void* output) = 0;

  template <typename T>
  T doSomething() {
    assert(type() == typeid(T));
    T result;
    doSomethingVoid(&result);
    return result;
  }

  virtual ~IDynamicBase() {}
};

请注意,它有一个名为doSomething模板 方法,它采用类型参数作为返回值。这是我们稍后会调用的方法。

现在,修改您之前的 IBaseTest 以扩展 IDynamicBase

template <class T1>
class IBaseTest : public IDynamicBase
{
public:
  std::type_index type() const {return typeid(T1);}

  void doSomethingVoid(void* output) {
    *(reinterpret_cast<T1*>(output)) = DoSomething();
  }

  virtual T1 DoSomething() = 0;
  virtual ~IBaseTest() {}
};

您无需更改CBaseTest1CBaseTest2

最后,您现在可以像这样在 main 函数中编写代码:

  IDynamicBase* pBase = nullptr;

  pBase = new CBaseTest1();

  std::cout << pBase->doSomething<int>() << std::endl;

  pBase = new CBaseTest2();

  std::cout << pBase->doSomething<long long>() << std::endl;

请注意,我们现在调用pBase-&gt;doSomething&lt;T&gt;(),而不是调用pBase-&gt;DoSomething(),其中T 是一种必须在调用方法时静态知道的类型,并且我们在调用站点提供该类型,例如pBase-&gt;doSomething&lt;int&gt;().

【讨论】:

  • 但是,它基本上没有用处,因为您需要知道用于实际派生类的类型。那时,只能使用派生类...
  • 好吧,假设您阅读了一个文本文件,其中每个标记都有自己的 IBaseTest,其中 T 取决于标记是否成功解析为字符串、浮点数、布尔值或整数。然后也许 DoSomething 会返回解析后的值。作为加载和解析标记的结果,您会得到一个 std::vector<:shared_ptr>>。如果你想计算文本文件中所有浮点数的总和,你可以遍历元素'e',并且只有当 e->type() == typeof(float) 时,你才执行 sum += e->doSomething ().
  • 如果您检测到类型,那么您也可以直接调用CBaseTest1::DoSomething() 获取int 并使该函数虚拟!如果您只想计算所有浮点数中的一部分,那么Visitor 设计模式可能更合适,否则您的代码会在代码中的许多地方进行类型检查。
  • 对我来说,原始发帖人似乎试图在 C++ 中进行动态输入。如果这真的是他想要做的,也许他应该看看 std::any 什么的。我注意到了你的回答,你不需要像我那样滥用类型系统,这很好。但是,我可能错了,但是您建议的访问者技术不开放扩展,因为您需要在 IVisitor 类中预先声明所有感兴趣的类型。所以无论我们如何做,都会有权衡,这在编程中最常见。
  • 我没有使用过any,也可以使用variant,它可以使用访问者:en.cppreference.com/w/cpp/utility/variant/visit。正确的选择取决于实际问题...
【解决方案2】:

该语言不允许直接执行您正在尝试执行的操作。此时,您应该问问自己这是否是解决问题的正确方法。

假设您没有为每种类型执行太多不同的操作,第一种可能效果很好的方法是简单地在函数本身中执行操作,而不是返回通过继承不相关的类型。

class IBaseTest
{
public:
    virtual void OutputTo(std::ostream &os) = 0;
};

class CBaseTest1
{
public:
    virtual void OutputTo(std::ostream &os) override;
private:
    int DoSomething();
};

void CBaseTest1OutputTo(std::ostream &os)
{
    os << DoSomething() << std::endl;
}

如果你只有几种类型但操作很多,你可能会使用访问者模式。

如果你主要有依赖于类型的操作,你可以使用:

class IVisitor
{
public:
    virtual void Visit(int value) = 0;
    virtual void Visit(long value) = 0;
};

否则,使用更通用的方法

class IVisitor
{
public:
    virtual void Visit (CBaseTest1 &test1) = 0;
    virtual void Visit (CBaseTest2 &test2) = 0;    
};

然后在你的类中添加一个应用函数

class IBaseTest
{
public:
    virtual void Apply(IVisitor &visitor) = 0;
};

在每个派生类中,您实现 Apply 函数:

void CBaseTest1 : public IBaseTest
{
    virtual void Apply(IVisitor &visitor) override 
    {
        visitor.Visit(this->DoSomething()); // If you use first IVisitor definition
        visitor.Visit(*this);               // If you use second definition
};

出于创建目的,如果您需要从文件创建这些类,则可以有一个工厂从类型标签返回适当的类……

假设您每次都想要一个新对象的示例:

 enum class TypeTag { Integer = 1, LongInteger = 2 };
std::unique_ptr<IBaseTest> MakeObjectForTypeTag(TypeTag typeTag)
{
    switch (typeTag)
    {
        case TypeTag::Integer : return new CBaseTest1();
        case TypeTag::LongInteger : return new CBaseTest2();
    }
}

因此,您唯一会执行switch 语句的时候是在创建对象时...您也可以为此使用映射甚至数组...

正确的方法取决于您的实际问题。

  • 您有多少个 CBaseClass*?
  • 您希望添加其他类吗?经常?
  • 你有多少类似于DoSomething()的操作?
  • 您有多少对DoSomething 的结果有效的操作?
  • 您希望添加其他操作吗?经常?

通过回答这些问题,您会更容易做出正确的决定。如果动作是稳定的(你只有几个),那么像OutputToabove 这样的特定虚函数更合适。但是,如果您有十几个操作但不期望 ITestBase 类层次结构有太大变化,那么访问者解决方案更合适。

给定解决方案在给定上下文中更合适的原因主要是将来添加类或操作时的维护工作。您通常希望最频繁的更改(添加类或操作)需要在代码中的所有位置进行更改。

【讨论】:

    猜你喜欢
    • 2012-10-24
    • 1970-01-01
    • 2018-01-10
    • 1970-01-01
    • 1970-01-01
    • 2019-07-25
    • 1970-01-01
    • 1970-01-01
    • 2017-09-22
    相关资源
    最近更新 更多