Non-Static Member Functions
Syntax#
-
// Calling:
- variable.member_function();
- variable_pointer->member_function();
-
// Definition:
- ret_type class_name::member_function() cv-qualifiers {
- body;
- }
- ret_type class_name::member_function() cv-qualifiers {
-
// Prototype:
- class class_name {
- virt-specifier ret_type member_function() cv-qualifiers virt-specifier-seq;
- // virt-specifier: “virtual”, if applicable.
- // cv-qualifiers: “const” and/or “volatile”, if applicable.
- // virt-specifier-seq: “override” and/or “final”, if applicable.
- }
- class class_name {
Remarks#
A non-static
member function is a class
/struct
/union
member function, which is called on a particular instance, and operates on said instance. Unlike static
member functions, it cannot be called without specifying an instance.
For information on classes, structures, and unions, please see the parent topic.
Non-static Member Functions
A class
or struct
can have member functions as well as member variables. These functions have syntax mostly similar to standalone functions, and can be defined either inside or outside the class definition; if defined outside the class definition, the function’s name is prefixed with the class’ name and the scope (::
) operator.
class CL {
public:
void definedInside() {}
void definedOutside();
};
void CL::definedOutside() {}
These functions are called on an instance (or reference to an instance) of the class with the dot (.
) operator, or a pointer to an instance with the arrow (->
) operator, and each call is tied to the instance the function was called on; when a member function is called on an instance, it has access to all of that instance’s fields (through the this
pointer), but can only access other instances’ fields if those instances are supplied as parameters.
struct ST {
ST(const std::string& ss = "Wolf", int ii = 359) : s(ss), i(ii) { }
int get_i() const { return i; }
bool compare_i(const ST& other) const { return (i == other.i); }
private:
std::string s;
int i;
};
ST st1;
ST st2("Species", 8472);
int i = st1.get_i(); // Can access st1.i, but not st2.i.
bool b = st1.compare_i(st2); // Can access st1 & st2.
These functions are allowed to access member variables and/or other member functions, regardless of either the variable or function’s access modifiers. They can also be written out-of-order, accessing member variables and/or calling member functions declared before them, as the entire class definition must be parsed before the compiler can begin to compile a class.
class Access {
public:
Access(int i_ = 8088, int j_ = 8086, int k_ = 6502) : i(i_), j(j_), k(k_) {}
int i;
int get_k() const { return k; }
bool private_no_more() const { return i_be_private(); }
protected:
int j;
int get_i() const { return i; }
private:
int k;
int get_j() const { return j; }
bool i_be_private() const { return ((i > j) && (k < j)); }
};
Encapsulation
A common use of member functions is for encapsulation, using an accessor (commonly known as a getter) and a mutator (commonly known as a setter) instead of accessing fields directly.
class Encapsulator {
int encapsulated;
public:
int get_encapsulated() const { return encapsulated; }
void set_encapsulated(int e) { encapsulated = e; }
void some_func() {
do_something_with(encapsulated);
}
};
Inside the class, encapsulated
can be freely accessed by any non-static member function; outside the class, access to it is regulated by member functions, using get_encapsulated()
to read it and set_encapsulated()
to modify it. This prevents unintentional modifications to the variable, as separate functions are used to read and write it. [There are many discussions on whether getters and setters provide or break encapsulation, with good arguments for both claims; such heated debate is outside the scope of this example.]
Name Hiding & Importing
When a base class provides a set of overloaded functions, and a derived class adds another overload to the set, this hides all of the overloads provided by the base class.
struct HiddenBase {
void f(int) { std::cout << "int" << std::endl; }
void f(bool) { std::cout << "bool" << std::endl; }
void f(std::string) { std::cout << "std::string" << std::endl; }
};
struct HidingDerived : HiddenBase {
void f(float) { std::cout << "float" << std::endl; }
};
// ...
HiddenBase hb;
HidingDerived hd;
std::string s;
hb.f(1); // Output: int
hb.f(true); // Output: bool
hb.f(s); // Output: std::string;
hd.f(1.f); // Output: float
hd.f(3); // Output: float
hd.f(true); // Output: float
hd.f(s); // Error: Can't convert from std::string to float.
This is due to name resolution rules: During name lookup, once the correct name is found, we stop looking, even if we clearly haven’t found the correct version of the entity with that name (such as with hd.f(s)
); due to this, overloading the function in the derived class prevents name lookup from discovering the overloads in the base class. To avoid this, a using-declaration can be used to “import” names from the base class into the derived class, so that they will be available during name lookup.
struct HidingDerived : HiddenBase {
// All members named HiddenBase::f shall be considered members of HidingDerived for lookup.
using HiddenBase::f;
void f(float) { std::cout << "float" << std::endl; }
};
// ...
HidingDerived hd;
hd.f(1.f); // Output: float
hd.f(3); // Output: int
hd.f(true); // Output: bool
hd.f(s); // Output: std::string
If a derived class imports names with a using-declaration, but also declares functions with the same signature as functions in the base class, the base class functions will silently be overridden or hidden.
struct NamesHidden {
virtual void hide_me() {}
virtual void hide_me(float) {}
void hide_me(int) {}
void hide_me(bool) {}
};
struct NameHider : NamesHidden {
using NamesHidden::hide_me;
void hide_me() {} // Overrides NamesHidden::hide_me().
void hide_me(int) {} // Hides NamesHidden::hide_me(int).
};
A using-declaration can also be used to change access modifiers, provided the imported entity was public
or protected
in the base class.
struct ProMem {
protected:
void func() {}
};
struct BecomesPub : ProMem {
using ProMem::func;
};
// ...
ProMem pm;
BecomesPub bp;
pm.func(); // Error: protected.
bp.func(); // Good.
Similarly, if we explicitly want to call a member function from a specific class in the inheritance hierarchy, we can qualify the function name when calling the function, specifying that class by name.
struct One {
virtual void f() { std::cout << "One." << std::endl; }
};
struct Two : One {
void f() override {
One::f(); // this->One::f();
std::cout << "Two." << std::endl;
}
};
struct Three : Two {
void f() override {
Two::f(); // this->Two::f();
std::cout << "Three." << std::endl;
}
};
// ...
Three t;
t.f(); // Normal syntax.
t.Two::f(); // Calls version of f() defined in Two.
t.One::f(); // Calls version of f() defined in One.
Virtual Member Functions
Member functions can also be declared virtual
. In this case, if called on a pointer or reference to an instance, they will not be accessed directly; rather, they will look up the function in the virtual function table (a list of pointers-to-member-functions for virtual functions, more commonly known as the vtable
or vftable
), and use that to call the version appropriate for the instance’s dynamic (actual) type. If the function is called directly, from a variable of a class, no lookup is performed.
struct Base {
virtual void func() { std::cout << "In Base." << std::endl; }
};
struct Derived : Base {
void func() override { std::cout << "In Derived." << std::endl; }
};
void slicer(Base x) { x.func(); }
// ...
Base b;
Derived d;
Base *pb = &b, *pd = &d; // Pointers.
Base &rb = b, &rd = d; // References.
b.func(); // Output: In Base.
d.func(); // Output: In Derived.
pb->func(); // Output: In Base.
pd->func(); // Output: In Derived.
rb.func(); // Output: In Base.
rd.func(); // Output: In Derived.
slicer(b); // Output: In Base.
slicer(d); // Output: In Base.
Note that while pd
is Base*
, and rd
is a Base&
, calling func()
on either of the two calls Derived::func()
instead of Base::func()
; this is because the vtable
for Derived
updates the Base::func()
entry to instead point to Derived::func()
. Conversely, note how passing an instance to slicer()
always results in Base::func()
being called, even when the passed instance is a Derived
; this is because of something known as data slicing, where passing a Derived
instance into a Base
parameter by value renders the portion of the Derived
instance that isn’t a Base
instance inaccessible.
When a member function is defined as virtual, all derived class member functions with the same signature override it, regardless of whether the overriding function is specified as virtual
or not. This can make derived classes harder for programmers to parse, however, as there’s no indication as to which function(s) is/are virtual
.
struct B {
virtual void f() {}
};
struct D : B {
void f() {} // Implicitly virtual, overrides B::f.
// You'd have to check B to know that, though.
};
Note, however, that a derived function only overrides a base function if their signatures match; even if a derived function is explicitly declared virtual
, it will instead create a new virtual function if the signatures are mismatched.
struct BadB {
virtual void f() {}
};
struct BadD : BadB {
virtual void f(int i) {} // Does NOT override BadB::f.
};
As of C++11, intent to override can be made explicit with the context-sensitive keyword override
. This tells the compiler that the programmer expects it to override a base class function, which causes the compiler to omit an error if it doesn’t override anything.
struct CPP11B {
virtual void f() {}
};
struct CPP11D : CPP11B {
void f() override {}
void f(int i) override {} // Error: Doesn't actually override anything.
};
This also has the benefit of telling programmers that the function is both virtual, and also declared in at least one base class, which can make complex classes easier to parse.
When a function is declared virtual
, and defined outside the class definition, the virtual
specifier must be included in the function declaration, and not repeated in the definition.
This also holds true for override
.
struct VB {
virtual void f(); // "virtual" goes here.
void g();
};
/* virtual */ void VB::f() {} // Not here.
virtual void VB::g() {} // Error.
If a base class overloads a virtual
function, only overloads that are explicitly specified as virtual
will be virtual.
struct BOverload {
virtual void func() {}
void func(int) {}
};
struct DOverload : BOverload {
void func() override {}
void func(int) {}
};
// ...
BOverload* bo = new DOverload;
bo->func(); // Calls DOverload::func().
bo->func(1); // Calls BOverload::func(int).
For more information, see the relevant topic.
Const Correctness
One of the primary uses for this
cv-qualifiers is const
correctness. This is the practice of guaranteeing that only accesses that need to modify an object are able to modify the object, and that any (member or non-member) function that doesn’t need to modify an object doesn’t have write access to that object (whether directly or indirectly). This prevents unintentional modifications, making code less errorprone. It also allows any function that doesn’t need to modify state to be able to take either a const
or non-const
object, without needing to rewrite or overload the function.
const
correctness, due to its nature, starts at the bottom up: Any class member function that doesn’t need to change state is declared as const
, so that it can be called on const
instances. This, in turn, allows passed-by-reference parameters to be declared const
when they don’t need to be modified, which allows functions to take either const
or non-const
objects without complaining, and const
-ness can propagate outwards in this manner. Due to this, getters are frequently const
, as are any other functions that don’t need to modify logical state.
class ConstIncorrect {
Field fld;
public:
ConstIncorrect(const Field& f) : fld(f) {} // Modifies.
const Field& get_field() { return fld; } // Doesn't modify; should be const.
void set_field(const Field& f) { fld = f; } // Modifies.
void do_something(int i) { // Modifies.
fld.insert_value(i);
}
void do_nothing() { } // Doesn't modify; should be const.
};
class ConstCorrect {
Field fld;
public:
ConstCorrect(const Field& f) : fld(f) {} // Not const: Modifies.
const Field& get_field() const { return fld; } // const: Doesn't modify.
void set_field(const Field& f) { fld = f; } // Not const: Modifies.
void do_something(int i) { // Not const: Modifies.
fld.insert_value(i);
}
void do_nothing() const { } // const: Doesn't modify.
};
// ...
const ConstIncorrect i_cant_do_anything(make_me_a_field());
// Now, let's read it...
Field f = i_cant_do_anything.get_field();
// Error: Loses cv-qualifiers, get_field() isn't const.
i_cant_do_anything.do_nothing();
// Error: Same as above.
// Oops.
const ConstCorrect but_i_can(make_me_a_field());
// Now, let's read it...
Field f = but_i_can.get_field(); // Good.
but_i_can.do_nothing(); // Good.
As illustrated by the comments on ConstIncorrect
and ConstCorrect
, properly cv-qualifying functions also serves as documentation. If a class is const
correct, any function that isn’t const
can safely be assumed to change state, and any function that is const
can safely be assumed not to change state.