C++ Trick, Anyone ever tried this?

Started by
12 comments, last by Valakor 4 years, 9 months ago

I'm assuming this is completely illegal but I was wondering if anyone else has ever used it. It actually seems kind of useful or at least I can think of uses for it. In fact I remember a few years back wanting this feature but at that time I didn't realize you could actually do it. So what's going on below is we are changing the v-table using placement new. Here's the code.....


// ChangeClass.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include "pch.h"
#include <assert.h>
#include <iostream>

/**
 ****************************************************************************************************
 **
 ****************************************************************************************************
 **/

class CGod
{

public:

   CGod() 
   {
   }

   CGod(int iA, int iB, int iC, int iD) :
      m_iA(iA), m_iB(iB), m_iC(iC), m_iD(iD)
   {}
      
   void DataIntact() 
   {
       std::cout << "Data Intact! (" << m_iA << "," << m_iB << "," << m_iC << "," << m_iD << ")\n";
   }

   virtual void WhoAmI() 
   {
      std::cout << "God: \"I am a random god!\"\n";
      DataIntact();
   }
   
   void *operator new (size_t size,  void* ptr) 
   { 
      return (void *) ptr; 
   }
  
   // Keep the compiler from complaining
   void operator delete(void* addr,  void* ptr) 
   {
      assert(0);
   }

   virtual ~CGod() {}


public:

   int m_iA;
   int m_iB;
   int m_iC;
   int m_iD;

};



/**
 ****************************************************************************************************
 **
 ****************************************************************************************************
 **/

class CBrahma : public CGod
{

public:

   CBrahma() {}

   void WhoAmI()  override
   {
      std::cout << "Brahma: \"Now I am Brahma!\"\n";
      DataIntact();
   }

   static void Change( CGod *pGod)
   {
      new(pGod) CBrahma();
   }

};

/**
 ****************************************************************************************************
 **
 ****************************************************************************************************
 **/

class  CRama : public CGod
{

public:

   CRama() {}

   void WhoAmI()  override
   {
      std::cout << "CRama: \"Now I am Rama!\"\n";
      DataIntact();
   }

   static void Change( CGod *pGod)
   {
      new(pGod) CRama();
   }

};

/**
 ****************************************************************************************************
 **
 ****************************************************************************************************
 **/

class  CVishnu : public CGod
{

public:

   CVishnu() {}

   void WhoAmI()  override
   {
      std::cout << "Vishnu: \"Now I am become Death, the destroyer of worlds!\"\n";
      DataIntact();
   }

   static void Change( CGod *pGod)
   {
      new(pGod) CVishnu();
   }

};

/**
 ****************************************************************************************************
 **
 ****************************************************************************************************
 **/


int main()
{
   CGod *pGod = ::new CGod(1,2,3,4);

   pGod->WhoAmI();
   CBrahma::Change(pGod);
   pGod->WhoAmI();
   CRama::Change(pGod);
   pGod->WhoAmI();
   CVishnu::Change(pGod);
   pGod->WhoAmI();

   ::delete pGod;

}

And here's the output ......
 


God: "I am a random god!"
Data Intact! (1,2,3,4)
Brahma: "Now I am Brahma!"
Data Intact! (1,2,3,4)
CRama: "Now I am Rama!"
Data Intact! (1,2,3,4)
Vishnu: "Now I am become Death, the destroyer of worlds!"
Data Intact! (1,2,3,4)

 

 

Advertisement

Apart from the fact that you are not calling the destructor when you change the god and that you are not checking for the size of the allocated memory, this should be perfectly legal. That is what placement new is for, except for the fact that you normally use a buffer instead of a class instance. If you add a data member to any of the derived classes, this will break though. Another problem can arise when you have multiple base classes as this approach would not give an error if a cast from derived to a base class pointer requires dynamic_cast.

9 minutes ago, Magogan said:

Apart from the fact that you are not calling the destructor when you change the god and that you are not checking for the size of the allocated memory, this should be perfectly legal. That is what placement new is for, except for the fact that you normally use a buffer instead of a class instance. If you add a data member to any of the derived classes, this will break though. Another problem can arise when you have multiple base classes as this approach would not give an error if a cast from derived to a base class pointer requires dynamic_cast.

Well..... I know there are lots of ways to break it.  But still, if you are careful, it's an easy way to change the behavior of an existing object.

5 minutes ago, Gnollrunner said:

Well..... I know there are lots of ways to break it.  But still, if you are careful, it's an easy way to change the behavior of an existing object.

If you need to know what you are doing, it is not exactly easy. To achieve this, you could also just create a new instance or use a std::function or use pre-allocated instances of each class.

It's an easy trick in the same way as driving above the speed limit is an easy trick to get to your destination faster. It technically works, but if you're not careful (or unlucky), bad things can happen.

8 minutes ago, Magogan said:

If you need to know what you are doing, it is not exactly easy. To achieve this, you could also just create a new instance or use a std::function or use pre-allocated instances of each class.

It's an easy trick in the same way as driving above the speed limit is an easy trick to get to your destination faster. It technically works, but if you're not careful (or unlucky), bad things can happen.

However a new instance is exactly that. It's not the same object.  Here we are changing the behavior of an existing object.  Also it does it in one fell swoop. You swap out the whole v-table, so 20 different functions can change at the same time. Also it gains whatever optimizations that the compiler puts in for virtual function calls.

It is legal but comes with certain drawbacks. The reason it is fully legal is because it is just memory, regardless if it is an object on the stack or heap, memory is in the first place just a chunk of the smalest possible data entity (bytes). Placement new dosen't care of the kind of pointer you pass to it, it just constructs the object and this is ok because it opens a world of memory manipulation possibilities you don't have in more strict languages like C# for example.

I used this in my task scheduler and also am using it still in any class that manages the nodes of a tree structure like a JSON document or a text filter. The document just inherits from Array<byte> and adds just space management functions. Nodes are added to the document by using placement new to the buffer and certain offset to the buffer pointer. I then can easily exchange the type of node in the document without any bigger trouble.

However, my memory allocator for example operates just with placement new only. The default (heap) allocator just handles chunks of bytes using malloc/free to aquire the memory, any other allocator is based on a pre-allocated buffer. It then returns the pointer into a generic function and constructs the object


template<class T> inline T* Allocate(uint16 alignment)
{
    return new (Allocate(sizeof(T), alignment)) T();
}

 

3 minutes ago, Shaarigan said:

It is legal but comes with certain drawbacks. 

Legal as in it generally works.  But I'm guessing it falls under undefined behavior. It's kind of like calling a member function with a null pointer.  As long as you don't reference though "this", or use any data members it will run fine.  It's even useful as you can put the check inside the function as opposed to outside. Microsoft famously used this in their system code. However ...... I think it's technically not guaranteed to work although it works on every compiler I've ever used.

7 minutes ago, Gnollrunner said:

But I'm guessing it falls under undefined behavior

I would bet the bevahior isn't undefined because you technically construct an object in already allocated block of memory. Imagine the CGod pointer as void*, you are free to cast it to CGod, CBrahma or whatever and this is what you do here. You tell the compiler that you know at this time, that the pointer is of kind CGod, so the behavior here is well defined.

1 hour ago, Gnollrunner said:

we are changing the v-table

you are not changing the vtable but the pointer that is placed in your object's memory. If you force cast the object to something else then the vtable will have wrong jump offsets anyways.

On the other hand you tell placement new that you have a pointer to memory and that you know at this time that the chunk will match the object you want to construct. This is well defined behavior too.

So the gap between those two processes is what may be undefined but it is as well undefined as if someone would cast CGod to anything it can't be cast to and end up with a mangled vtable

23 minutes ago, Shaarigan said:

 

you are not changing the vtable but the pointer that is placed in your object's memory.

Let me rephrase that..... I meant changing as in "changing your underwear" .  You are changing which vtable your object is using.

In any case the reason why it could be undefined behavior is that I'm not sure if a compiler is allowed to clear memory when you do a new.  Most, maybe all C++ compilers won't do this because of the performance hit, but I can imagine it might be legal, especially in some sort of debug mode. However maybe it's in the standard somewhere that it won't. 

A compiler dosen't "clear" any memory except you are in debug mode on windows for example. Otherwise you have to assume that you always get uninitialized memory anyways. A placement new/new simply places the vtable pointer on top of your object and calls the constructor, anything else is up to your class.

You can do a simple test, just create a new object instance with an int property for simplicity but do not initialize that int. In debug mode on Windows Visual Studio you'll get a zero integer when inspecting the object, in release mode it can be any kind of number due to uninitialized memory

This topic is closed to new replies.

Advertisement