Home > database >  How does the compiler know which virtual function to call in this situation?
How does the compiler know which virtual function to call in this situation?

Time:01-10

I'm studying for an exam and I have this code given to me:

#include <iostream>
#include <string>
#include <cmath>

using namespace std;

class Expression {
public:
    Expression() = default;
    Expression(const Expression&) = delete;
    Expression& operator=(const Expression&) = delete;
    virtual ~Expression() {}
    virtual double eval()const = 0;
    virtual void print(ostream& out)const = 0;
    friend ostream& operator<<(ostream& out, const Expression& e) {
        // cout << "@";
        e.print(out);
        return out;
    }
};

class BinaryExpression : public Expression {
    Expression* _e1, * _e2;
    char _sign;
    virtual double eval(double d1, double d2)const = 0;
public:
    BinaryExpression(Expression* e1, Expression* e2, char sign) : _e1(e1), _e2(e2), _sign(sign) {}
    ~BinaryExpression() override { delete _e1; delete _e2; }
    virtual double eval()const override {
        cout << "BE eval" << endl;
        return eval(_e1->eval(), _e2->eval());
    }
    virtual void print(ostream& out)const override {
        out << '(' << *_e1 << _sign << *_e2 << ')';
    }
};

class Sum : public BinaryExpression {
    virtual double eval(double d1, double d2)const override {
        cout << "Sum private eval" << endl;
        return d1   d2;
    }
public:
    Sum(Expression* e1, Expression* e2) : BinaryExpression(e1, e2, ' ') {}
};

class Exponent : public BinaryExpression {
    virtual double eval(double d1, double d2)const override {
        cout << "E private eval" << endl;
        return std::pow(d1, d2);
    }
public:
    Exponent(Expression* e1, Expression* e2) : BinaryExpression(e1, e2, '^') {}
};

class Number : public Expression {
    double _d;
public:
    Number(double d) : _d(d) {}
    virtual double eval()const override {
        cout << "Num eval" << endl;
        return _d;
    }
    virtual void print(ostream& out)const override {
        out << _d;
    }
};

int main() {
    Expression* e = new Sum(
        new Exponent(
            new Number(2),
            new Number(3)),
        new Number(-2));

    cout << *e << " = " << e->eval() << endl;
    delete e;
}

I used the debugger to see which lines are executed but I'm still wondering how to the compiler knew which function to call each time in the main() where we call e->eval()

Output:

BE eval
Num eval
BE eval
Num eval
Num eval
E private eval
Sum private eval
((2^3) -2) = 6

Given every class has an eval function some of them even have 2 and using the Expression pointer threw me off a bit. What exactly does the compiler search for when looking for which eval() to run each time?

CodePudding user response:

There are two topics in your question:

How does the compiler know which function to use, given some classes have multiple functions with the same name?

Well, they may have the same name, but they don't have the same signature. There is no ambiguity between eval() and eval(x,y) calls, because there is only one eval that doesn't accept any arguments and only one eval that accepts two arguments.

Given Expression* e how does the compiler know which function to call in e->eval() expression?

The answer is that the compiler does not know. This happens at runtime, not during compilation. Unless an advanced optimization technique, called devirtualization, applies (which is a big topic that I'm not going to talk about here).

Typically1 when define virtual functions on a class, your compiler will store additional data within each object of that type, so called vtable, which is just an array of function pointers. Then when you do e->eval() on a virtual method, the compiler will replace this call with two steps: (1) get function pointer from the vtable stored in e object corresponding to eval virtual method, (2) call that function pointer with e object (and potentially other arguments).

1: this is an implementation detail, one of the possible strategies, not necessarily what happens exactly.

CodePudding user response:

Virtual functions work based on a VTable.

In short, when declaring your class, it actually requires the class to have an array of pointers to the actual functions. Instead of a regular call to the function, it first gets this address from the array and than calls it.

In practice, virtual classes have 1 extra pointer in the class that refers to that array (the VTable) and when your class is constructed, that pointer is filled in with the address of the VTable of that class (calculated at compile time).

So the simple mental model: it's pointers to functions instead of to data with some compiler magic behind it.

If you really want, you can emulate this in C using function pointers and a lot of manual code. It for sure would be a good exercise to learn about them.

  •  Tags:  
  • Related