Obtain the vtable of a class without an object
Asked Answered
L

1

6

I am attempting to implement a system similar to the first described here. That is, the (ab)use of vtable modification to change object behavior at runtime. This is part of my attempts to create an efficient type-generic wrapper in a C++ project I am working on.

The example, should you be unable to access it, copies the vtable using memcpy() and the this pointer as such:

void setType( const DataType& newType )
{
    memcpy( this, &newType, sizeof(DataType) );
}

However, I have an issue with this method: I do not have an object of the target class to copy the vtable from, and do not want to have to create one, as some types are costly to construct.

Is there a way to access the vtable which would be emplaced into an object of a given class without an object of that class?

It would be preferable if it were somewhat portable, but I have largely resigned to this being compiler-specific; as such, a GCC/G++ only method would be acceptable if there is no other option. Let us also assume I am only concerned with building this on fairly standard OSes and architectures.

I am using C++11, should that aid in this somehow.

Edit: I want to be completely clear, I know how dangerous this sort of behavior is. I'm more interested in the idea and perhaps its narrow application in very controlled circumstances than I am in it being a good idea for production software, despite what my intro might suggest.

Labiodental answered 4/3, 2016 at 16:48 Comment(10)
Surely you need an instance of the object AT SOME point, so why not just modify it AFTER it's been created, then? [Although it seems like a terrible idea in general to modify vtables, which may not even be stored in writeable memory, and compilers these days do track the objects and skip over vtable access if it "knows" what to call]Prolocutor
If you wan't to change the object instances behavior at runtime rather implement the State Design Pattern. What you want isn't possible in a portable way, the compiler isn't required to implement dynamic polymorphism using a vtable at all acccording the c++ standard.Carolincarolina
Two ideas: (1) PIMPL, where you create an object without the impl stuff to get the vtable (using some special ctor for that). That would make it lightweight to create a "dummy" object just for the vtable, not actual use. (2) Implement a templated factory function which has a local static variable, holding a dummy instance (kind of singleton) just to get the vtable. So you have only one object of each type as your overhead in total, which I guess is okay. (1+2) They can be even combined, but I guess it's not necessary.Spectrochemistry
@MatsPetersson I'm aware this is built on nonstandard behavior, hence I pointed out this is an "abuse" of vtables. I'm trying to evaluate if, aside from this issue I brought up, it's a viable concept on the platforms I am interested in. The very possible issues don't really affect my question. - As for having to have an instance, no, I don't: for example, I have a child class to wrap an integer, but I never create an instance of that child class. I just emplace its vtable when an integer is put into the wrapper. Effectively, I morph the base class into an inherited class every time it is set.Labiodental
In the linked code, the object is nothing but its vtable. Second, consider placement new and discriminating unions.Cheka
@Yakk That's the example, but not my reality. If I did use this, I'd have a set amount of memory for the wrapper (64 bits), and possibly have some (dynamic memory) objects wrapped like vectors and strings with their pointers stored in that memory. Placement new is a good idea, I hadn't thought of that.Labiodental
@WilliamKappler So you want a discriminating union with some type-erased operations (in your case, via vtable). Why use vtables at all and break standard, why not a manual vtable with type erasure? The above technique still needs forwarding methods -- why forward in an illegal way?Cheka
If compiler devirtualize the call, you hack won't work neither on this part.Manuscript
@Yakk I'm trying to avoid conditional checks on a type enum at runtime, which would potentially be costly, by modifying the vtable to use the functions that would behave correctly for a given wrapped type. However, I'm not sure I fully understand your comment. Do you have any examples of this being done using that method?Labiodental
@Manuscript There's several ways to prevent that in GCC, but for the most part it already won't if it can't confirm the object type. So virtual functions are generally safe to begin with when called outside the class. But again, this is to an extent a thought experiment and test. I'd rather play around with it before dismissing it outright.Labiodental
C
4
struct many_vtable {
  void(*dtor)(void*);
  void(*print)(void const*,std::ostream&);
};
template<class T>struct tag_t{};
template<class T>constexpr tag_t<T> tag = {};

template<class T>
many_vtable const* make_many_vtable(tag_t<T>){
  static const many_vtable retval = {
    // dtor
    [](void* p){
      reinterpret_cast<T*>(p)->~T();
    },
    // print
    [](void const*p, std::ostream& os){
      os<<*reinterpret_cast<T const*>(p);
    }
  };
  return &retval;
}
struct many {
  many_vtable const* vtable=nullptr;
  std::aligned_storage_t<100, alignof(double)> buff;
  void clear(){if(vtable) vtable->dtor(&buff);vtable=nullptr;}
  ~many(){ clear(); }
  many()=default;
  many(many const&)=delete; // not yet supported
  many& operator=(many const&)=delete; // not yet supported

  explicit operator bool()const{return vtable!=nullptr;}

  template<class T,class...Args>
  void emplace(Args&&...args){
    static_assert(alignof(T) <= alignof(double), "not enough alignment");
    static_assert(sizeof(T) <= 100, "not enough size");
    clear();
    ::new((void*)&buff) T(std::forward<Args>(args)...);
    vtable=make_many_vtable(tag<T>);
  }
  friend std::ostream& operator<<(std::ostream& os, many const&m){
    if(!m.vtable) return os;
    m.vtable->print(&m.buff, os);
    return os;
  }
};

This is a manual vtable design. It can store anything up to 100 bytes whose alignment is less than a double that can be printed to a stream.

'Erasing' down to more (or different) operations is easy. For example, being able to copy/move to another many.

It does not violate the standard, and it has similar overhead to the linked example.

many m;
m.emplace<int>(3);
std::cout << m << '\n';
m.emplace<double>(3.14);
std::cout << m << '\n';

live example.

This is manual vtables, because we are basically reimplementing the vtable concept manually.

Cheka answered 4/3, 2016 at 17:54 Comment(1)
Although not a direct answer to the question I posed, this does seem more useful to my stated use case than what I was investigating. As such, I'll accept it as the answer. Thanks for writing up the example.Labiodental

© 2022 - 2024 — McMap. All rights reserved.