What Are Early Binding and Late Binding?

Every function call has to end up somewhere. Early binding (also called static binding) means the compiler works out which function definition a call refers to while the program is being built. Late binding (also called dynamic binding) means that decision cannot be made until the program is running.

Both mechanisms exist in ordinary C++ code, and you have already used both without naming them. This lesson looks at the machinery, because late binding is exactly what makes a virtual call able to reach a derived override.

A call needs a destination

Compiling a function turns it into a block of machine instructions, and that block sits at some position in the finished program. That position is the function's entry point. Calling the function means the CPU stops running the current block, transfers control to that entry point, runs the callee, and comes back.

So every call site needs an answer to one question: where does control transfer to? There are only two ways to supply that answer.

Where the destination comes from When it is known What the CPU does
Written into the call instruction itself Build time Transfers control immediately
Stored somewhere the program can read Run time Loads the destination first, then transfers control

The first row is early binding. The second row is late binding. Everything else in this lesson is a consequence of that one split.

Two words that get mixed up

A C++ program is full of names, and each name carries a set of properties. Declaring int quantity{} tells the compiler that the name quantity has type int, occupies storage, and so on. Attaching properties to a name like that is binding in the general sense.

Function binding is the specific case of working out which function definition a given call refers to. Actually transferring control to the chosen definition is dispatching. C++ programmers usually fold dispatching into the word binding and talk about a call being "bound early" or "bound late", and this lesson follows that habit.

Nomenclature
Binding means several unrelated things in C++. Attaching a reference to an object is binding, std::bind produces a callable, and a language binding is a bridge between two languages. None of those are the binding discussed here.

Early binding: the destination is fixed at build time

A direct call names the function it wants. Calls to non-member functions and to non-virtual member functions are direct calls, and the compiler resolves each of them by looking at the name and the argument types. Once resolved, the destination is baked into the generated code.

#include <iostream>
#include <string>
#include <string_view>

std::string padRight(std::string_view caption, std::size_t width)
{
    std::string slot{ caption };
    if (slot.size() < width)
        slot.append(width - slot.size(), '.');

    return slot;
}

struct Terminal
{
    std::size_t width{};

    void writeRow(std::string_view caption) const
    {
        std::cout << padRight(caption, width) << '|' << '\n';
    }
};

int main()
{
    std::cout << padRight("depth", 12) << '|' << '\n';

    Terminal panel{ 12 };
    panel.writeRow("bearing");

    return 0;
}

Output:

depth.......|
bearing.....|

Both calls are early bound. padRight("depth", 12) names a non-member function, and panel.writeRow("bearing") names a non-virtual member function. Nothing about either destination can change while the program runs, so the compiler and linker can emit an instruction that jumps straight to the entry point.

Overload resolution and template instantiation happen at build time too, so calls to overloaded functions and to function templates are also early bound. The compiler picks the winner from the argument types and then generates a direct call to that one function.

#include <iostream>

void describe(int reading)
{
    std::cout << "int overload chosen for " << reading << '\n';
}

void describe(double reading)
{
    std::cout << "double overload chosen for " << reading << '\n';
}

template <typename T>
void describe(T reading)
{
    std::cout << "generic overload chosen for " << reading << '\n';
}

int main()
{
    describe(7);     // int overload wins
    describe(2.5);   // double overload wins
    describe('x');   // generic overload wins

    return 0;
}

Output:

int overload chosen for 7
double overload chosen for 2.5
generic overload chosen for x

Three call sites, three different destinations, and every one of them settled before the program ever ran. Overloading and templates are sometimes described as compile-time polymorphism for exactly this reason.

Late binding: the destination is read from storage

Late binding needs somewhere to keep a destination that the program can change. A function pointer is the simplest such place: it is a pointer whose pointee is a function rather than an object, and applying the call operator () to it transfers control to whatever function it currently refers to. That is an indirect call.

The example below stores three different formatting functions in one array and calls all three through a single call site.

#include <iostream>
#include <string>
#include <string_view>

std::string padLeft(std::string_view caption, std::size_t width)
{
    std::string slot{ caption };
    if (slot.size() < width)
        slot.insert(0, width - slot.size(), '.');

    return slot;
}

std::string padCentre(std::string_view caption, std::size_t width)
{
    std::string slot{ caption };
    if (slot.size() >= width)
        return slot;

    std::size_t band{ (width - slot.size()) / 2 };
    slot.insert(0, band, '.');
    slot.append(width - slot.size(), '.');

    return slot;
}

std::string padRight(std::string_view caption, std::size_t width)
{
    std::string slot{ caption };
    if (slot.size() < width)
        slot.append(width - slot.size(), '.');

    return slot;
}

int main()
{
    using PadStyle = std::string (*)(std::string_view, std::size_t);

    const PadStyle styles[]{ padLeft, padCentre, padRight };

    for (PadStyle style : styles)
        std::cout << '[' << style("kilo", 10) << ']' << '\n';

    return 0;
}

Output:

[......kilo]
[...kilo...]
[kilo......]

There is one call expression in this program, style("kilo", 10), and it reached three separate functions. No destination could have been written into it during the build, because the destination depends on how far the loop has got. Instead the program reads the pointer, then transfers control to whatever entry point it found.

Notice what late binding buys: the set of behaviours is now data. Adding a fourth style means adding a fourth element to styles, not adding a fourth branch to a chain of if statements.

Virtual calls take the same route

Function pointers are the visible form of late binding. Virtual functions are the form the language manages for you. When you call a virtual function through a pointer or reference to a base class, the destination is chosen from the object's actual type at runtime, which is why the term dynamic dispatch is used for this case.

#include <iostream>

struct Gauge
{
    virtual ~Gauge() = default;

    void frame() const
    {
        std::cout << "Gauge frame" << '\n';
    }

    virtual void needle() const
    {
        std::cout << "Gauge needle" << '\n';
    }
};

struct DialGauge : public Gauge
{
    void frame() const
    {
        std::cout << "DialGauge frame" << '\n';
    }

    void needle() const override
    {
        std::cout << "DialGauge needle" << '\n';
    }
};

int main()
{
    DialGauge dial{};
    const Gauge& face{ dial };

    face.frame();
    face.needle();

    return 0;
}

Output:

Gauge frame
DialGauge needle

Two calls through the same reference, two different outcomes. frame() is not virtual, so it was bound early from the static type of face, which is const Gauge&. needle() is virtual, so it was bound late from the object face actually refers to. The next lesson takes apart the table of addresses the compiler builds to make that second call work.

Choosing a branch at runtime is not late binding

This distinction trips people up. A program can decide at runtime which call site executes while every one of those call sites is still early bound.

#include <iostream>
#include <iterator>
#include <string_view>

void shout(std::string_view caption)
{
    std::cout << caption << " !!" << '\n';
}

void whisper(std::string_view caption)
{
    std::cout << '(' << caption << ')' << '\n';
}

int main()
{
    const std::string_view captions[]{ "dock ready", "hatch open", "fuel low" };

    for (std::size_t slot{ 0 }; slot < std::size(captions); ++slot)
    {
        if (slot % 2 == 1)
            whisper(captions[slot]);
        else
            shout(captions[slot]);
    }

    return 0;
}

Output:

dock ready !!
(hatch open)
fuel low !!

Which line runs depends on the loop counter, so the behaviour certainly varies at runtime. Binding does not. There are two call sites here, one naming shout and one naming whisper, and the compiler nailed down both destinations. The if selects a path through code that was already fully resolved.

Warning
"Decided at runtime" and "bound at runtime" are different claims. Ask which function definition a single call expression can reach. If the answer is exactly one, that call is early bound no matter how much branching surrounds it.

Which calls bind when

Call form Binding Why
Call to a non-member function by name Early Name and arguments identify one definition
Call to a non-virtual member function Early Chosen from the static type of the object expression
Call to an overloaded function Early Overload resolution runs during compilation
Call to a function template Early The instantiation is generated and called directly
Call through a function pointer Late Destination lives in a pointer the program can change
Virtual call through a base pointer or reference Late Destination depends on the object's dynamic type
Virtual call on an object whose type is known exactly Early No ambiguity is left for runtime to resolve

That last row is worth a second look. Writing dial.needle() on a DialGauge object rather than through a base reference gives the compiler the complete answer, so no runtime lookup is needed even though needle() is virtual.

Cost against flexibility

Early binding Late binding
Steps at the call site Transfer control Read the destination, then transfer control
Relative speed Slightly faster Slightly slower, one extra level of indirection
Inlining Possible Usually blocked, the callee is unknown
Set of reachable functions Fixed when you build Can be chosen, swapped, or extended at runtime

The extra indirection is small, and one extra memory read per call is rarely what makes a program slow. The lost inlining opportunity matters more in tight loops, because an inlined call can vanish into surrounding code while an indirect call cannot.

Best Practice
Let the requirement pick the mechanism. If a call site only ever needs one function, name it directly. Reach for a function pointer or a virtual function when the choice genuinely belongs to runtime, and accept the indirection as the price of that flexibility.

Summary

Binding and dispatching: Binding attaches properties to names. Function binding is the process of working out which function definition a call refers to, and dispatching is the act of transferring control to it.

Early binding: A direct call to a non-member function or a non-virtual member function is resolved during compilation. The compiler and linker can then generate an instruction that jumps straight to the function's address. Overload resolution and template instantiation also happen at build time, so those calls are early bound too.

Late binding: A call that cannot be resolved until the program runs is late bound. Calling through a function pointer is the explicit form, and a virtual call through a base pointer or reference is the form the language provides, usually called dynamic dispatch.

Function pointers: A function pointer holds the address of a function, and calling through it is an indirect call. The compiler cannot name the destination at that call site, so the destination is read from the pointer at runtime.

Runtime branching is not late binding: An if or switch choosing between direct calls still leaves every call site bound at compile time. What matters is how many definitions a single call expression can reach.

Trade-off: Late binding costs one extra level of indirection and usually rules out inlining, and it repays that with the ability to decide, replace, or extend behaviour while the program runs.

The next lesson opens up the virtual table, the structure the compiler builds so a virtual call can find the right override.