Monkey Patching changing code at runtime (replacing or adding methods, altering prototypes/classes/modules) without editing the original source. Powerful and dynamic — but risky. Use it deliberately and carefully.

Languages that commonly allow/encourage monkey patching

  • Ruby — class reopening and Module#prepend / alias_method make it a classic.

  • Python — functions, methods, modules are mutable objects; you can replace names at runtime. Testing helpers like unittest.mock/pytest facilitate it.

  • JavaScript — modify prototypes, replace functions on objects, or change global objects (e.g., Array.prototype). Node and browsers both permit it.

  • Smalltalk — historically the original environment for live system modification.

  • Perl, Lua, PHP, R — dynamic languages where functions/objects can be re-bound at runtime.

  • Objective-C / Swift (ObjC runtime) — “method swizzling” is the Obj-C cousin of monkey patching.

  • Java/.NET — not naturally dynamic, but runtime instrumentation agents (Java agents, .NET profiling APIs, etc.) can achieve similar effects for tooling or hotfixes.

(If a language exposes runtime binding or reflection it’s often possible — but idioms and safety vary widely.)

Use cases beyond “hacking” or one-off patches

  1. Testing (mocks/stubs/isolation)

    • Replace network/database calls with deterministic fakes. Tools like pytest’s monkeypatch exist because this is a legitimately valuable pattern.

    • Temporarily swap environment variables, time functions, etc.

  2. Polyfills / Compatibility shims

    • Add or replace methods to provide newer APIs on older runtimes (e.g., browser polyfills, backporting features to older library versions).
  3. Instrumentation & Observability

    • Add logging, metrics, tracing to third-party libraries when you can’t or don’t want to change their source. (Prefer hooking APIs when available.)
  4. A/B experiments & feature flags

    • Swap implementations at runtime to route some users to an experimental path without a full deploy.
  5. Adapters for integration

    • Make a third-party library match your app’s expected interface by wrapping or replacing a few methods (temporary bridge until a proper adapter is written).
  6. Prototyping & REPL-driven development

    • Rapidly try alternative implementations without restarting long-running processes — common in Ruby / Smalltalk / live servers.
  7. Backward/forward compatibility in libraries

    • Libraries sometimes provide selective runtime replacements to adapt to host environment quirks.
  8. Hotfixing in production (emergency, with caution)

    • In extreme cases ops teams may apply runtime fixes to a live process to block a catastrophe. This is high-risk and should be replaced by a proper code change ASAP.
  9. Domain-specific language (DSL) / metaprogramming

    • Add convenience methods to make fluent APIs or DSLs nicer (e.g., Rails’ ActiveSupport extensions).
  10. Educational / debugging tools

    • Temporarily change behavior to demonstrate concepts, trace execution, or capture additional debug info.

Risks and costs

  • Fragility / surprising interactions. Other code may rely on original behavior.

  • Hard-to-debug bugs — breakages can be global and time-dependent, making test coverage/diagnosis harder.

  • Upgrades can silently break your patches when the underlying library changes implementation or behaviour.

  • Security surface — if untrusted code can monkeypatch your runtime, it’s a vector for compromise.

  • Thread-safety / concurrency issues when swapping implementations in multi-threaded environments.

  • Maintenance debt — future devs may not expect runtime behavior to be altered.

Best practices (how to monkeypatch safely)

  • Prefer safer alternatives first: configuration, dependency injection, subclassing, adapters, decorators, proxies, official extension points or hooks.

  • Limit scope: patch a single instance rather than a global class/prototype if possible.

  • Feature-detect instead of version-detect: check behavior presence before changing things.

  • Create reversible patches: record original implementations and restore them after tests or when no longer needed.

  • Document heavily: why the patch exists, who owns it, and when it should be removed.

  • Isolate in tests and CI: ensure your patches are covered by tests and run in CI to detect breakage.

  • Namespace and avoid global mutation where possible (e.g., in JS, avoid mutating Object.prototype/Array.prototype).

  • Use community tooling: many languages have standard mock/monkeypatch helpers that are safer than ad-hoc replacements.

When you should monkeypatch

  • Tests where isolation is needed.

  • Small, well-documented compatibility shims or polyfills.

  • Instrumentation when no plugin/hook exists and you own the runtime.

  • Short-lived prototyping or REPL development.

  • Emergency production hotfixes only if you accept the risk and revert quickly.

When you shouldn’t

  • As a long-term substitute for proper design (use dependency injection, extension points, or pull requests to upstream libs).

  • On widely shared global objects in large codebases without strict controls.

  • If there’s a supported, less risky extension mechanism.

Monkey Patching in C++

Monkey patching is not a normal or standard method of programming in C++. The practice of modifying or extending code at runtime without changing the source code is fundamentally at odds with C++’s design as a statically typed, compiled language.

While monkey patching is a known—though often controversial—technique in dynamic languages like Python and Ruby, it is strongly discouraged in C++ due to its complexity and severe drawbacks.

Why monkey patching is not normal in C++

  • Static vs. dynamic: C++ is a statically typed language where the behavior of code is typically determined at compile-time. Monkey patching relies on dynamic, runtime changes.
  • Encouraged alternatives: C++ offers many standard, reliable, and type-safe alternatives to runtime code modification. These include:
    • Inheritance: You can extend an existing class and override its virtual functions in a controlled and predictable way.
    • Composition: You can build new functionality by composing existing objects.
    • Dependency Injection: You can pass dependencies explicitly, making it easier to replace them for testing or other purposes.
    • Templates: You can create generic code that operates on different types at compile-time.
  • Technical difficulty: Achieving a true “monkey patch” in C++ is a complex and often non-portable process that goes against the language’s design philosophy. It can require techniques such as:
    • Overwriting memory directly, like an object’s virtual function table (vtable).
    • Using operating system-specific tools like LD_PRELOAD on Linux or DLL injection on Windows.
  • Serious drawbacks: Even when possible, monkey patching in C++ carries significant risks:
    • Brittleness and fragility: Runtime patches are not part of the source code and can break silently with future library or compiler updates.
    • Reduced predictability: It can lead to surprising behavior and make the code’s execution path much harder to reason about and debug.
    • Global state issues: Modifying a class affects all instances of that class, potentially causing unforeseen side effects across the entire program. 

When C++ techniques are used for similar purposes Instead of monkey patching, a C++ programmer might employ specific techniques to achieve isolated, more controlled versions of dynamic behavior, particularly for debugging or testing:

  • Link-time replacement: You can replace a function’s implementation by providing your own version that gets linked into the final executable.
  • Function pointers or std::function: You can design your classes to call a function through a pointer. This pointer can then be changed at runtime to point to a different function. This is a deliberate, explicit design pattern, not a hack.
  • Preprocessor macros (#define): For debugging or testing private class members, a developer can use a preprocessor macro to temporarily change access specifiers. This is not a runtime patch, but a modification at compile-time. 

How vtable monkey-patching works

When a class has virtual functions, the compiler creates a data structure called a vtable, which contains an array of function pointers. Each object of that class then gets a hidden pointer, the vptr, that points to its corresponding class’s vtable.

To perform monkey-patching, you must do the following:

  1. Get the vtable pointer: Use a cast to treat the object’s memory address as a pointer to a pointer, with the first element being the vptr.
  2. Access the vtable: Dereference the vptr to get a pointer to the vtable itself. The vtable is a static array of function pointers.
  3. Overwrite the function pointer: Locate the specific function pointer you want to patch and replace it with a pointer to your new, custom function.

When the patched object calls the virtual function, the vptr still points to the (now modified) vtable, which dispatches the call to your custom function instead of the original one.

Why vtable patching is brittle

  1. Compiler and architecture dependency

The vtable is an implementation detail of the compiler, not part of the C++ standard. Its layout, including the size and position of function pointers, can change with:

  • Different compilers: A vtable hack written for GCC will likely fail on MSVC or Clang because they can arrange vtables differently.
  • Compiler settings: Changing optimization flags (-O1, -O2, etc.) can alter the vtable layout. For example, some virtual calls may be inlined, skipping the vtable entirely and making a patch ineffective.
  • Architecture: A layout for a 32-bit architecture may not work on a 64-bit one due to changes in pointer size.
  1. Class hierarchy changes

Any modification to the class’s virtual functions, such as adding, removing, or reordering a virtual method, will change the layout of the vtable. This breaks existing patches because the indices used to overwrite function pointers will no longer point to the correct function.

  1. Object construction and destruction

During object construction and destruction, the compiler performs internal vtable swaps.

  • Constructors: As a derived class is built, its vtable is incrementally updated. A vtable patch applied during construction could be overwritten by this process.
  • Destructors: Virtual calls made from a base class’s destructor execute the base class’s version of the function, as the derived part of the object has already been destroyed. A monkey-patch could lead to undefined behavior, such as a segmentation fault, if it attempts to access destroyed data.
  1. Multiple and virtual inheritance

When a class inherits from multiple or virtual base classes, the vtable layout becomes more complex, often requiring multiple vtables per object. The logic to find and manipulate the correct vtable pointer would need to be re-engineered, dramatically increasing complexity and fragility.

  1. Security vulnerabilities Vtable patching is a well-known method for malware to hijack program execution flow by corrupting vtable pointers. Modern compilers and operating systems have deployed exploit mitigation techniques to prevent or detect these types of attacks, which can interfere with and break any legitimate attempts at vtable modification.

  2. Incompatibility with dynamic linking If a library is dynamically loaded, the compiler may not have full knowledge of the final class hierarchy at compile time. This makes it difficult to reliably predict vtable layouts for dynamically linked classes. A minor change to the shared library could completely break a vtable patch.

The bottom line

Vtable patching bypasses the high-level type system in C++ and depends on specific, low-level implementation details that can change unpredictably. It sacrifices stability and robustness for a temporary, fragile hack. While it can sometimes offer a way to get around limitations in binary-only code, it is never a suitable solution for an extensible, scalable, or maintainable codebase.

All that having been said, let’s monkey patch in c++

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdint.h>
#include <iostream>

class Object {
public:
  virtual void doThing() {
    std::cout << "In doThing" << std::endl;
  }
};

void dynamicOverride(Object *a) {
  std::cout << "In dynamicOverride" << std::endl;
}

int main(int argc, char *argv[]) {
  Object *a = new Object();

  // grab the vtable for the Object class
  void **vtable = *(void ***)a;

  a->doThing();
  // out: "I'm an doThing"

  void (Object::*ptr)() = &Object::doThing;
  void *offset = *(void **)&ptr;
  vtable[((uintptr_t)offset) / sizeof(void *)] = (void *)&dynamicOverride;

  a->doThing();
  // out: "In dynamicOverride"

  // affects future instances as well
  Object *a2 = new Object();
  a->doThing();
  // out: "In dynamicOverride"

  return 0;
}

Explain how this code works

1
2
3
4
Object* a = new Object();

// grab the vtable for the Object class
void** vtable = *(void***)a;
  1. Object* a = new Object();

This line just creates a new object of type Object on the heap and returns a pointer to it.

  • a points to the start of the object in memory.

  • If class Object has virtual functions, the compiler automatically includes a hidden pointer (often called the vptr) at the start of the object’s memory layout.

  • The vptr points to the vtable — a table of function pointers used for virtual dispatch.

  1. void** vtable = *(void***)a;

This line is doing some pointer type gymnastics to get at that vtable.

Let’s decode it step by step:

Step 1a is of type Object* But we’re not interested in Object’s type; we want to interpret its memory as a pointer to a pointer to a pointer to void (void***).

Step 2(void***)a We cast the Object* pointer to void***.
Why three levels?

Because: - a points to the object → type Object* - The first thing in the object (if it has virtual functions) is the vptr, which itself is a pointer to the vtable → type void** - So, interpreting a as a pointer to a pointer (void***) lets us dereference once to get the actual vtable pointer.

Step 3*(void***)a

This dereference gives us the vtable pointer itself.

That vtable pointer is an array of function pointers (void* values), hence the final type:

1
void** vtable;
  1. What vtable now points to: After this line executes:
1
void** vtable = *(void***)a;

vtable now points to an array of pointers — each pointer corresponds to the implementation address of a virtual function in Object (or its most derived class).

For example, you could inspect or even call functions from it manually (dangerous, but possible):

1
2
3
using Fn = void(*)(Object*);
Fn f0 = (Fn)vtable[0];  // first virtual function
f0(a);                  // manually call it

Summary

Expression Meaning
Object* a = new Object(); Create an object of Object
(void***)a Treat the object pointer as a pointer to a pointer to a pointer to void
*(void***)a Dereference once to get the vtable pointer
void** vtable A pointer to an array of function pointers (the vtable)

Clownie, but it works

This technique is highly unscalable and brittle because it subverts the compiler’s memory management and type safety, leading to unpredictable failures with even minor changes. And I know I should have used a reinterpret_cast.