【问题标题】:Derived class method from base class pointer : some alternatives?从基类指针派生类方法:一些替代方案?
【发布时间】:2026-02-02 09:00:01
【问题描述】:

我知道这类问题已经回答过几次,但我给出的问题背景是为了期待其他一些架构替代方案。

考虑一个类 CExpression:

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

CExpression 必须将表示数学表达式(例如“y = x + 5”)的字符串分解为向量(“y”、“=”、“x”、“+”、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_;
        ...
}

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

其实由于我只能使用指针类型类的方法(除非我是dynamic_cast,我认为这不是最好的方式),我有两个问题:

  1. 是否有合适的方法来做我想要实现的目标,或者 dynamic_cast 是我唯一的选择?
  2. 我应该采用完全不同的程序架构来解决这个问题吗?如果是,我该怎么办?

顺便说一句,我知道解释起来可能更简单,但我认为上下文将是解决问题的好帮手。

谢谢!

【问题讨论】:

  • 我认为树形结构更适合您想要捕捉的各种表达方式。
  • 我想有一些选择:a)您可能需要考虑在基类中使用 all 方法,使无效的方法抛出异常或返回错误它们不适用,b) 您可能要考虑使用 visitor patterncommand pattern... 两者都是在通用界面下隐藏不同行为的情况。
  • 这是访问者模式的经典案例。

标签: c++ polymorphism


【解决方案1】:

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

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

y = x + 5

应该翻译成树:

  =
 / \
y   +
   / \
  x   5

这种结构有什么好处?首先,它比标记向量更容易评估。其次,诸如操作员优先级或关联方向之类的事情仅在构建此结构时才重要 - 在构建结构并准备好进行评估时不会使用它们。然后,每个节点不关心作为子节点附加到它的什么,它只是让它们评估自己,当它完成时,它最终会得到一个它可以工作的终端项目列表。甚至赋值运算符也可以执行其工作(当然,如果您将某种包含变量列表的上下文传递给它)。

如果您使用著名的反向波兰表示法算法,创建这样的结构非常容易。

在您的情况下,我会投票赞成将您的数据结构完全重新排列为一个,这对于存储表达式来说要好得多。

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

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

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

结构的草稿

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

【讨论】:

    【解决方案2】:

    我认为设计很差,在这里我将解释原因:
    在您输入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 或任何适合您的域的接口。

    【讨论】:

    • 这对我来说很好。但是,您在 void f() 中实现结构的方式迫使您提前知道表达式中的运算符的顺序。换句话说,我不知道如何让它从字符串输入中工作,因此我首先使用向量来存储分解后的表达式。任何使用字符串表达式的实现示例?
    • 至于高级知识:您可以轻松添加具有“添加”操作的类 - 例如参见@Spook 答案。至于字符串输入:您需要编写一个算法来扫描输入文本,找到表达式的根(例如等号),然后创建新表达式并将其“添加”到总数中。请注意,当找到根时,您现在有两个表达式,left 和 right 可以在其中寻找根。这是一个递归操作。