Member Function Pointers and Binding

Explore advanced techniques for working with class member functions
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

Previously, we’ve seen how we can create pointers to free functions:

#include <functional>

void FreeFunction() {/*...*/}

int main() {
  std::function FunctionPtr{FreeFunction};
}

In large projects, it’s more common that we’ll be working with member functions - that is, functions that are defined as part of a class or struct:

class SomeClass {
  void MemberFunction() {/*...*/}
};

In this lesson, we’ll learn how to work with pointers to these types of functions, and manage the additional complexity that is involved.

Static Member Functions

Creating a pointer to a static member function is similar to creating a pointer to a free function. The main difference is that we additionally need to specify what class the function belongs to, using the scope resolution operator ::.

For example, a static function called Greet() in the Greeter class can be identified using Greeter::Greet. We can create a pointer to such a function in the usual way:

#include <iostream>
#include <functional>

class Greeter {
public:
  static void Greet() {
    std::cout << "Hello World";
  }
};

int main() {
  std::function FunctionPtr{Greeter::Greet};
  FunctionPtr();
}
Hello World

Non-Static Member Functions

As we’ve seen, non-static member functions behave a little differently from regular functions, in that they are called in the context of some object.

Below, we call GetName() in the context of PlayerOne, meaning when the function uses an identifier like Name, it will be accessing the Name variable of the PlayerOne object:

#include <string>
#include <iostream>

class Player {
 public:
  std::string GetName() { return Name; }
  std::string Name;
};

int main() {
  Player PlayerOne{"Anna"};
  std::cout << PlayerOne.GetName();
}

We can create a pointer to a member function in the same way. Note, when creating a pointer to a non-static member function, the & operator is required:

auto NameGetter{&Player::GetName};

The key difference arises when we want to invoke a non-static member function, as we need to provide the context object. This is an instance of the Player class, such as PlayerOne in our previous example. The std::mem_fn() helper within <functional> can help us with this.

Passing our function pointer to this helper returns a new callable object, which allows us to invoke our function by passing the context object as the first argument. The object returned by std::mem_fn() is flexible, allowing us to provide this argument by reference, pointer, or smart pointer:

#include <functional>
#include <iostream>
#include <string>

class Player {
 public:
  std::string GetName() { return Name; }
  std::string Name;
};

int main() {
  Player PlayerOne{"Anna"};
  auto NameGetter{std::mem_fn(&Player::GetName)};

  // By Reference
  std::cout << NameGetter(PlayerOne) << '\n';

  // By Pointer
  std::cout << NameGetter(&PlayerOne) << '\n';

  // By Smart Pointer
  auto Ptr{std::make_unique<Player>("John")};
  std::cout << NameGetter(Ptr);
}
Anna
Anna
John

If the member function has additional parameters, we provide those arguments after the context object:

#include <functional>
#include <iostream>

class Player {
 public:
  void TakeDamage(int Damage) {
    Health -= Damage;
  }
  int Health;
};

int main() {
  Player PlayerOne{100};
  auto InflictDamage{std::mem_fn(
    &Player::TakeDamage)};

  InflictDamage(PlayerOne, 75);

  std::cout << "Remaining Health: "
  << PlayerOne.Health;
}
Remaining Health: 25

Typing Member Functions

A pointer to a static member function has the same type signature as a pointer to a free function:

void FuncA() {}
bool FuncB(int, float) {}

class MyClass {
 public:
  static void FuncC() {}
  static bool FuncD(int, float) {}
};

int main() {
  // Free Functions
  void (*PtrA)(){FuncA}
  bool (*PtrB)(int, float){FuncB};

  // Static Functions
  void (*PtrC)(){MyClass::FuncC}
  bool (*PtrD)(int, float){MyClass::FuncD};
}

However, the type of a non-static member function is a little more complex, as it includes the identifier of the class the function is a member of:

class MyClass {
 public:
  void NonStaticFunction() {}
  bool FunctionWithParameters(int x, float y) {
    return true;
  }
};

int main() {
  // Non-Static Function
  void (MyClass::*PtrB)(){
    MyClass::NonStaticFunction};

  // Non-Static Function With Parameters
  bool (MyClass::*PtrC)(int, float){
    MyClass::FunctionWithParameters};
}

In practice, however, we’d typically type and transfer functions using the std::function helper, as we introduced in the previous lesson.

When using std::function with a non-static member, we include an instance of that member’s class as the first parameter:

#include <functional>

class MyClass {/*...*/}; int main() { // Static Function std::function<void()> PtrA{ MyClass::StaticFunction}; // Non-Static Function std::function<void(MyClass&)>{ &MyClass::NonStaticFunction}; // Non-Static Function With Parameters std::function<bool(MyClass&, int, float)> PtrC{ &MyClass::FunctionWithParameters}; }

Binding and Partial Application

When passing a member function to some other object to use as a callback, it can be quite cumbersome and restrictive to require that function to also provide the context object.

#include <functional>
#include <iostream>

class Player {
 public:
  std::string GetName() { return Name; }

  std::string Name;
};

void Logger(auto GetString) {
  std::cout << GetString(
    Context  // How do I get this argument?
  );
}

int main() {
  Player PlayerOne{"Anna"};
  Logger(std::mem_fn(&Player::GetName));
}

To solve this, we can use partial application. "Applying" a function refers to providing all of the required arguments and invoking it. With pretty much every function invocation we’ve performed, we’ve been fully applying it. That is, we provided all the arguments and invoked them immediately:

#include <iostream>

int Add(int x, int y) {
  return x + y;
}

int main() {
  std::cout << Add(1, 2);
}
3

But with partial application, we can divide the application of the function into different steps, performed at different times.

This relies on binding, which allows us to provide some (or all) of the function’s required arguments. This binding process returns a partially applied function which we can call later, and provide any remaining arguments if required.

In the following example, we have a function that accepts three arguments. We use std::bind_front() to provide the value of the first argument (1 in this case). This returns a partially applied function:

#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto Partial{std::bind_front(Add, 1)};
}

We can then later call this function, providing the remaining arguments:

#include <iostream>
#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto Partial{std::bind_front(Add, 1)};
  std::cout << Partial(2, 3);
}
6

We can bind multiple arguments:

#include <iostream>
#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto Partial{std::bind_front(Add, 1, 2)};
  std::cout << Partial(3);
}
6

We can bind all arguments, leaving only the invocation for later:

#include <iostream>
#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto Partial{std::bind_front(Add, 1, 2, 3)};
  std::cout << Partial();
}
6

We can reuse partially-applied functions:

#include <iostream>
#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto Partial{std::bind_front(Add, 1, 2)};
  std::cout << Partial(3) << ", " << Partial(4);
}
6, 7

We can also split the partial application across as many steps as needed:

#include <iostream>
#include <functional>

int Add(int x, int y, int z) {
  return x + y + z;
}

int main() {
  auto PartialA{std::bind_front(Add, 1)};
  auto PartialB{std::bind_front(PartialA, 2)};
  auto PartialC{std::bind_front(PartialB, 3)};
  std::cout << PartialC();
}
6

Binding Member Functions

Let’s solve the original problem by partially applying a member function using the context object. This absolves the caller from needing to provide it later.

When binding a member function, std::bind_front() includes the behavior of std::mem_fn(), allowing us to provide the context object as the first argument.

The bound function returned from std::bind_front() additionally allows us to provide this context object either by value or pointer.

If we pass our context object by value, std::bind_front() will create a copy of the object. When we later call our bound function, it will be called in the context of that copy.

Below, our Player was called "Anna" at the time we bound it. We later changed our name to "John", but because we passed PlayerOne by value to std::bind_front(), the copy within our bound function is still called "Anna":

#include <functional>
#include <iostream>

class Player {/*...*/};
void Logger(auto GetString) {/*...*/} int main() { Player PlayerOne{"Anna"}; auto BoundFunction{std::bind_front( &Player::GetName, PlayerOne // Passing by value )}; PlayerOne.Name = "John"; Logger(BoundFunction); }
Anna

Alternatively, we can bind to a pointer. This approach avoids the performance cost of creating a copy of our object.

Moreover, when we later invoke our function, it will be called in the context of the original object, reflecting any changes we've made to it:

#include <functional>
#include <iostream>

class Player {/*...*/};
void Logger(auto GetString) {/*...*/} int main() { Player PlayerOne{"Anna"}; auto BoundFunction{std::bind_front( &Player::GetName, &PlayerOne // Passing by pointer )}; PlayerOne.Name = "John"; Logger(BoundFunction); }
John

std::bind() and std::placeholders

The std::bind_front() function was added in C++20, so may not be available in all projects.

We can accomplish the same goals using std::bind() and std::placeholders. These utilities are slightly more complex, but give us more flexibility and are available in older language versions.

Our dedicated lesson on partial application covers this in detail:

Returning Member Functions

Like with any other function pointer, our functions can return pointers to member functions:

#include <functional>
#include <string>
#include <iostream>

class Character {/*...*/}; auto GetRenamer() { return std::mem_fn(&Character::SetName); } int main() { auto Rename{GetRenamer()}; Character Player; Rename(Player, "Anna"); std::cout << "Name: " << Player.Name; }
Anna

This can include binding if necessary:

#include <functional>
#include <string>
#include <iostream>

class Character {/*...*/}; auto GetRenamer(Character& Player) { return std::bind_front( &Character::SetName, &Player); } int main() { Character Player; auto RenamePlayer{GetRenamer(Player)}; RenamePlayer("Anna"); std::cout << "Name: " << Player.Name; }
Anna

When we have a member function that itself is returning a pointer to another member function of the same class, we can bind it to the this pointer. This ensures the returned function is called in the context of the same object that returned that function:

#include <functional>
#include <string>
#include <iostream>

class Character {
 public:
  auto GetRenamer() {
    return std::bind_front(
      &Character::SetName, this);
  }

  void SetName(std::string NewName) {
    Name = NewName;
  }

  std::string Name;
};

int main() {
  Character Player;

  auto RenamePlayer{Player.GetRenamer()};
  RenamePlayer("Anna");

  std::cout << "Name: " << Player.Name;
}
Anna

Let’s see a more practical example. We’ll update the utility AI example we introduced in the previous lesson, but this time our actions are member functions.

Below, our Monster class has a SuggestAction() function. It returns a function pointer with its recommended action, and updates a Utility integer, estimating the usefulness of that action:

// Monster.h
#pragma once
#include <functional>
#include <iostream>

class Monster {
 public:
  std::function<void()> SuggestAction(
    int& Utility
  ) {
    if (canHeal()) {
      Utility = 150;
      return std::bind_front(
        &Monster::Heal, this);
    } else if (canAttack()) {
      Utility = GetAttackDamage();
      return std::bind_front(
        &Monster::Attack, this);
    } else {
      Utility = 25;
      return std::bind_front(
        &Monster::RunAway, this);
    }
  }

  void Heal() {
    std::cout << "Healing...\n";
  }
  void Attack() {
    std::cout << "Attacking...\n";
  }
  void RunAway() {
    std::cout << "Running away...\n";
  }

 private:
  bool canHeal() { return false; }
  bool canAttack() { return false; }
  int GetAttackDamage() { return 100; }
};

Elsewhere in our code, we can ask a Monster what action it recommends, but only perform that action if it would be sufficiently impactful:

#include <iostream>
#include "Monster.h"

void Act(Monster& Enemy) {
  int Utility;
  auto Action{Enemy.SuggestAction(Utility)};

  if (Utility >= 100) {
    Action();
  } else {
    std::cout << "Utility (" << Utility
      << ") is too low, taking no action\n";
  }
}

int main() {
  Monster Enemy;
  Act(Enemy);
}
Utility (25) is too low, taking no action

Summary

In this lesson, we expanded our knowledge of function pointers to include pointers to member functions. We covered:

  • Creating pointers to static and non-static member functions
  • Using std::mem_fn() to create callable objects from member function pointers
  • Employing std::bind_front() for partial application of functions
  • Returning member function pointers from other functions
  • Practical applications in game development scenarios

Was this lesson useful?

Next Lesson

SDL2 Timers and Callbacks

Learn how to use callbacks with SDL_AddTimer() to provide functions that are executed on time-based intervals
Abstract art representing programming
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Ticks, Timers and Callbacks
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 55 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

SDL2 Timers and Callbacks

Learn how to use callbacks with SDL_AddTimer() to provide functions that are executed on time-based intervals
Abstract art representing programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved