从基类指针派生类方法:一些替代方案?

时间:2014-05-09 05:33:02

标签: c++ polymorphism

我知道这类问题已经回答了几次,但我提出问题的背景,期待其他一些架构选择。

考虑一个CExpression类:

class CExpression
{
    public:
        ...
    private:
        vector<CComponent*> components_;
        string expression_;
}

CExpression必须将字符串分解为向量中的数学表达式(例如&#34; y = x + 5&#34;)(&#34; y&#34;,&#39; =& #39;,&#34; x&#34;,&#39; +&#39;,5)。为此,向量由CComponent指针组成,它可以指向类CVariable,COperator和CConstant的对象。 显然,CComponent是一个抽象类,也是上面提到的三个类的基类。因此,在解析字符串之后,向量应按顺序包含以下内容(进程的半伪代码):

components_.push_back(new CVariable("y"));
components_.push_back(new COperator('='));
components_.push_back(new CVariable("x"));
components_.push_back(new COperator('+'));
components_.push_back(new CConstant( 5 ));

这里使用多态性是将表达式分解为单个向量(这将有助于将来的解析过程)。但是,某些派生类具有其他人不具备的独特功能,因此无法在基类(CComponent)中实现这些功能。

例如,考虑COperator类:

class COperator : public CComponent
{
    public:
        int GetPriority() const { return prority_; }
        ...
    private:
        int priority_;
        ...
}

优先级,表示操作符必须从向量中解析的优先级,对于此类是唯一的(因此基类中没有虚函数)。现在让我们来解决问题。

考虑CComponent类(基类):

enum Type { VARIABLE, OPERATOR, CONSTANT };

class CComponent
{
    public:
        Type GetType() const { return type_; }
        ...
    private:
        Type type_;
        ...
}

类型,对于表达式的任何组件都是通用的,表示组件的类型(例如,如果它是CVariable,则在构造时类型将设置为VARIABLE)。

最后,考虑一下这种CExpression方法(虚构):

void CExpression::Process()
{
    for (int i = 0; i < components_.size(); i++)
    {
        if (components_[i] -> GetType() == OPERATOR)
        {
            cout << components_[i] -> GetPriority(); // won't work
        }
    }
}

事实上,由于我只能使用指针类型类的方法(除非我认为动态广播不是最美妙的方式),我有两个问题:

  1. 是否有正确的方法来实现我想要实现的目标,或者dynamic_cast是我唯一的选择?
  2. 我应该采用完全不同的程序架构来解决这个问题吗?如果是,我该怎么办?
  3. 顺便说一下,我知道解释起来可能更简单,但我认为上下文是抓住问题的好帮手。

    谢谢!

2 个答案:

答案 0 :(得分:3)

我想,您的架构无法满足您的需求 - 尤其是当您开始扩展它时。

我在处理数学表达式方面有一些经验,我说,存储表达式最自然的方式是树。每个终端项(例如数字或变量)是树的叶子,并且每个非终端项(例如运算符或函数调用)是具有子节点的节点。例如:

y = x + 5

应翻译成树:

  =
 / \
y   +
   / \
  x   5

这种结构有什么好处?首先,它比令牌矢量更容易评估。其次,运营商优先级或关联方向等问题仅在构建此结构时起作用 - 在构建结构并准备评估时不使用它们。然后,每个节点都不关心作为子节点附加到它上面的内容,它只是让它们自己进行评估,当它完成时,它最终会得到一个可以工作的终端项列表。甚至赋值运算符也可以执行它的工作(当然,如果你传递某种包含变量列表的上下文)。

如果使用众所周知的反向波兰表示法算法,那么创建这样的结构非常容易。

在你的情况下,我会对你的数据结构的完全重新安排进行投票,这对于存储表达式来说要好得多。

还有一件事。另外根据我的经验,我强烈建议您为这三件事创建不同的类

  • 从输入中读取的项目(=令牌)
  • 表达式树中保存的项目(=表达式项目)
  • 在评估树(= eval对象)时,项目是部分结果

这可能会使您的架构变得复杂,但实际上会简化您的工作并使您的架构更加灵活。

结构的草稿

class BaseNode
{
public:
    virtual EvalObject Eval() = 0;

    // This method is handy when working with assignment operator.
    // For instance, Eval() called on variable will return its value
    // but EvalLHS() will return a reference to variable. 
    virtual EvalObject EvalLHS() = 0;
};

class Operator : BaseNode
{

};

class BinaryOperator : Operator
{
private:
    BaseNode * leftChild;
    BaseNode * rightChild;
};

class Add : BinaryOperator
{
public:
    void Eval()
    {
        auto left = leftChild->Eval(); // Eval RHS, 
        auto right = rightChild->Eval(); // Eval RHS

        // Now perform calculations on left and right
        // depending on their types
    }

    void EvalLHS()
    {
        throw InvalidOperationException("Cannot perform LHS evaluation on adding operator");
    }
}

class Assign : BinaryOperator
{
public:
    void Eval()
    {
        auto left = leftChild->EvalLHS();
        auto right = rightChild->Eval();

        // Perform assignment

        // This is required such that operations
        // like a = b = 7 will also work
        return right;
    }

    void EvalLHS()
    {
        // Assignment cannot be on the LHS of operation, eg.
        // (a = 5) = 8 is wrong

        throw InvalidOperationException("Assignment cannot be LHS");
    }
}

答案 1 :(得分:0)

我觉得设计很差,在这里我会解释原因:
在您输入enum Type的那一刻,您实际上承认,虽然您希望在任何派生类上拥有一个纯粹的界面同样 - 但您无法做到。在基类中实现的算法确实需要 来知道派生的确切类型才能发挥作用。在这方面,enum Typedynamic_cast提供相同的习惯用法:依赖于确切类型的实现。
这不是面向对象的方式

面向对象的方式声称你的算法,你的代码,与一些基类一起工作的函数作为输入,不假设接口背后的真实对象 - 只有它的界面很重要。
至于你的具体问题,正如上面提到的,我也认为树状结构最适合这个问题。有几种方法可以做到这一点。在我看来,使用它有两个阶段:1。建立结构; 2.对结构进行一些评估 我将尝试仅绘制(不会编译)我的想法,细节和微调留给你:

class Expression
{
};    

class Constant : public Expression
{
    public:
        // 'int' can be easily changed to generic type 'T'
        Constant( int value ) : _value( value ) {}

    private:
        int _value;
};

class Operator : public Expression
{
    public:
        Operator( Expression left, Expression right )
            : _left( left ), _right( right ) {}

    protected:
        Expression _left;
        Expression _right;
};

class OperatorPlus : public Expression
{
    public:
        OperatorPlus( Expression left, Expression right )
            : Operator(_left( left ), _right( right )) {} 
};

// few more operators, the same
class OperatorMinus : public Expression { /* ... */ }
class OperatorMul : public Expression { /* ... */ }
class OperatorDiv : public Expression { /* ... */ }


class Variable : public Expression
{
    public:
        // string can be easily changed to generic type 'T'
        Constant( string value ) : _value( value ) {}

    private:
        string _value; 
}


void f()
{
    // y = x + 5
    OperatorEqual s1( Variable( "y" ), OperatrPlus( Variable( "x" ), Constant( 5 ) ) );
}

至于实际用它做什么。我认为最好的方法是在Expression类中添加所需的功能:

class Expression
{
    public:
        virtual Expression eval() = 0;
}

和派生可以轻松实现它:

class OperatorPlus : public Expression
{
    public:
        OperatorPlus( Expression left, Expression right )
            : Operator(_left( left ), _right( right )) {} 

    virtual Expression eval()
    {
        return _left.eval() + _right.eval();
    }
}

Expression的其他接口可能是printshortencombineEqual,也可能适合您的域名。