C++ Class Construct  «Prev  Next»
Lesson 5 External member functions
Objective Rewrite the C++ Person class using external member functions

External Member Functions in C++

An external member function is a member function that is declared inside a class but defined outside the class body using the scope resolution operator (::). The word "external" here has nothing to do with the extern linkage keyword. It simply means the function's implementation lives outside the curly braces of the class definition.

This separation of declaration from definition is foundational to how professional C++ projects are organized. The class header exposes the interface; the source file hides the implementation. The pattern scales from small educational examples all the way to distributed object systems where class interfaces are published across process or network boundaries.

Why Define Member Functions Externally?

When a member function is defined inside the class body, the compiler treats it as implicitly inline. Moving the definition outside the class changes three things:

Readability. A class declaration that contains only prototypes is easier to scan. Developers can see the full interface at a glance without scrolling past implementation details.

Compilation. Separating declarations (in a .h header) from definitions (in a .cpp source file) enables independent compilation. In large projects this can dramatically reduce rebuild times because a change to a function body does not force every translation unit that includes the header to recompile.

Encapsulation. The function prototype is visible in the header, but the implementation details remain hidden in the source file. This is the classic interface-versus-implementation separation that underpins both object-oriented design and modern C++20 module exports.

The Rule: Declare Inside, Define Outside

C++ requires that every member function be declared within its class definition. You cannot add new member functions to a class from outside. What you can do is provide the function's body externally by qualifying the function name with the class name and the scope resolution operator.

This rule preserves encapsulation: the class definition is the single authoritative source of its interface. The compiler uses name mangling to generate unique symbols for each member function, and that process depends on seeing the declaration inside the class. Allowing ad-hoc redeclaration from outside would break symbol resolution and scatter the interface across the codebase.

Person Class — Inline vs. External

Consider a simple Person class where every member function is defined inside the class body. Each of these functions is implicitly inline:


// Person.h — all member functions defined inline
#include <iostream>
#include <string>

class Person {
public:
    Person(std::string name, int age)
        : name_{std::move(name)}, age_{age} {}

    void print() const {
        std::cout << "Name: " << name_
                  << ", Age: " << age_ << '\n';
    }

    std::string getName() const { return name_; }
    int         getAge()  const { return age_;  }

private:
    std::string name_;
    int         age_;
};
 

Now rewrite the class using external member functions. The header contains only declarations; the source file contains the definitions:


// Person.h — declarations only
#ifndef PERSON_H
#define PERSON_H

#include <string>

class Person {
public:
    Person(std::string name, int age);
    void print() const;
    std::string getName() const;
    int         getAge()  const;

private:
    std::string name_;
    int         age_;
};

#endif // PERSON_H
 


// Person.cpp — external definitions
#include "Person.h"
#include <iostream>

Person::Person(std::string name, int age)
    : name_{std::move(name)}, age_{age} {}

void Person::print() const {
    std::cout << "Name: " << name_
              << ", Age: " << age_ << '\n';
}

std::string Person::getName() const { return name_; }

int Person::getAge() const { return age_; }
 

Notice that each external definition uses the Person:: qualifier. Without it, the compiler would treat the function as a free (non-member) function. Also note that print() is no longer implicitly inline — which leads directly to the topic of explicit inlining.

From External Definitions to Explicit Inline Functions

When you move a member function outside the class body, it loses its implicit inline status. In most cases this is exactly what you want: the function lives in a single .cpp translation unit, the linker sees one definition, and compilation is straightforward.

But sometimes you want both the organizational clarity of an external definition and the performance hint of inlining. That is the purpose of explicit inline functions.

What Is an Inline Function?

The idea behind inline functions is to insert the code of a called function at the point where it is called. If done carefully, this can improve performance in exchange for increased compile time and potentially larger binaries.

It is useful to distinguish between the inline keyword — which is a request to the compiler — and the act of being inlined, which is the actual expansion the compiler performs. The expansion matters more than the request, because the costs and benefits are associated with the expansion. The compiler is free to ignore the hint.

C++ guarantees that the observable behavior of a function does not change based on whether the compiler actually inlines it. This guarantee is important: it means the inline keyword is always safe to apply, even if the compiler ultimately decides not to expand the function.

C programmers may see a similarity between inline functions and #define macros, but the analogy breaks down quickly. Inline functions respect scope and type safety, can be stepped through in a debugger, and do not suffer from the side-effect hazards of macro parameter expansion. Macros are always expanded textually; inline functions are expanded at the compiler's discretion.

Explicit Inline Member Functions

The inline specifier can be applied explicitly to member functions defined at file scope. This keeps the class definition clean while still hinting to the compiler that the function is a good candidate for inlining.


struct ch_stack {
    // ...
    void reset();
    void push(char c);
    // ...
};

inline void ch_stack::reset() {
    top = EMPTY;
}

inline void ch_stack::push(char c) {
    assert(top != FULL);
    ++top;
    s[top] = c;
}
 

The same pattern applies when you want explicit inlining on a class with richer semantics. In explicit inlining, the keyword inline appears both on the prototype inside the class and on the definition outside it:


class Date {
public:
    inline Date(int mm, int dd, int yy);
    inline void setDate(int mm, int dd, int yy);
    void showDate();  // not inlined

private:
    int month_;
    int day_;
    int year_;
};

inline Date::Date(int mm, int dd, int yy)
    : month_{mm}, day_{dd}, year_{yy} {}

inline void Date::setDate(int mm, int dd, int yy) {
    month_ = mm;
    day_   = dd;
    year_  = yy;
}

void Date::showDate() {
    std::cout << month_ << '/' << day_ << '/' << year_;
}
 

The relationship is straightforward: an external member function is any member function defined outside the class body. An explicit inline function is an external member function that has been annotated with the inline keyword to request expansion at the call site. Every explicit inline function is an external member function, but not every external member function is inline.

Modern C++ Considerations (C++20 / C++23)

C++20 Modules. The header/source split that motivates external member functions has a modern successor: modules. A module interface unit exports the class declaration, while the module implementation unit contains the external definitions. The compilation model changes — no more #include guards, no more header re-parsing — but the fundamental pattern of declaring inside and defining outside remains identical.


// person.cppm — module interface unit (C++20)
export module person;
import <string>;

export class Person {
public:
    Person(std::string name, int age);
    void print() const;
    std::string getName() const;
    int         getAge()  const;

private:
    std::string name_;
    int         age_;
};
  


// person.cpp — module implementation unit
module person;
import <iostream>;

Person::Person(std::string name, int age)
    : name_{std::move(name)}, age_{age} {}

void Person::print() const {
    std::cout << "Name: " << name_
              << ", Age: " << age_ << '\n';
}

std::string Person::getName() const { return name_; }
int Person::getAge() const { return age_; }
  

C++23 constexpr Expansion and if consteval. C++23 relaxes many restrictions on constexpr functions, allowing more complex logic to be evaluated at compile time. An external member function marked constexpr (or consteval) can be defined outside the class body just like any other member function. The if consteval construct lets a single function body provide different paths for compile-time and run-time evaluation.

C++23 Deducing this. The explicit object parameter feature (this auto&& self) replaces traditional const/non-const overload pairs with a single template definition. These deducing-this member functions follow the same internal/external definition rules, and they are particularly useful in CRTP (Curiously Recurring Template Pattern) hierarchies.

Distributed Objects. The separation of interface from implementation that external member functions enforce maps naturally onto distributed object architectures. Technologies such as CORBA, gRPC, and modern C++ RPC frameworks generate proxy classes whose member functions are declared in a header (or IDL-generated stub) and defined externally in a source file that marshals arguments across a network boundary. The class interface remains local; the implementation executes remotely. Understanding external member functions is a prerequisite for working with any of these systems.

The ch_stack Example Revisited

To connect back to the earlier module material, here is the ch_stack class with push defined externally. All other member functions remain implicitly inline because they are defined inside the class body:


const int max_len = 40;

class ch_stack {
public:
    void reset()   { top = EMPTY; }
    void push(char c);                 // declared only
    char pop()     { assert(top != EMPTY); return s[top--]; }
    char top_of()  { assert(top != EMPTY); return s[top]; }
    bool empty()   { return (top == EMPTY); }
    bool full()    { return (top == FULL);  }

private:
    char s[max_len];
    int  top;
    enum { EMPTY = -1, FULL = max_len - 1 };
};

// External (non-inline) definition
void ch_stack::push(char c) {
    assert(top != FULL);
    ++top;
    s[top] = c;
}
 

External Member Function — Exercise

Click the Exercise link below to rewrite the Person class so it uses an external member function to print out its member data.
External Member Function — Exercise


SEMrush Software