“动态”改变现有对象的VTBL,动态子类化

时间:2014-01-19 02:06:21

标签: c++ c++11 new-operator

请考虑以下设置。

基类:

class Thing {
  int f1;
  int f2;

  Thing(NO_INIT) {}
  Thing(int n1 = 0, int n2 = 0): f1(n1),f2(n2) {}
  virtual ~Thing() {}

  virtual void doAction1() {}
  virtual const char* type_name() { return "Thing"; }
}

通过实现上述方法,的派生类:

class Summator {
  Summator(NO_INIT):Thing(NO_INIT) {}

  virtual void doAction1() override { f1 += f2; }
  virtual const char* type_name() override { return "Summator"; }
}

class Substractor {
  Substractor(NO_INIT):Thing(NO_INIT) {}    
  virtual void doAction1() override { f1 -= f2; }
  virtual const char* type_name() override { return "Substractor"; }
}

我的任务要求能够动态更改现有对象的类(在本例中为VTBL)。如果我没有弄错,这就称为动态子类化。

所以我想出了以下功能:

// marker used in inplace CTORs
struct NO_INIT {}; 

template <typename TO_T>
    inline TO_T* turn_thing_to(Thing* p) 
    { 
      return ::new(p) TO_T(NO_INIT()); 
    }

就是这样 - 它使用inplace new来构造一个对象来代替另一个对象。实际上,这只会更改对象中的vtbl指针。所以这段代码按预期工作:

Thing* thing = new Thing();
cout << thing->type_name() << endl; // "Thing"
turn_thing_to<Summator>(thing);
cout << thing->type_name() << endl; // "Summator"
turn_thing_to<Substractor>(thing);
cout << thing->type_name() << endl; // "Substractor"

我采用这种方法的唯一主要问题是 a)每个派生类都应该有像Thing(NO_INIT) {}这样的特殊构造函数,它们应该完全没有任何作用。并且b)如果我想要将诸如std :: string之类的成员添加到Thing中它们将无法工作 - 只允许具有NO_INIT构造函数的类型作为Thing的成员。

问题:对于解决'a'和'b'问题的动态子类化是否有更好的解决方案?我有一种感觉,std :: move语义可能有助于以某种方式解决'b'但不确定。

以下是代码的ideone

8 个答案:

答案 0 :(得分:1)

对于您的代码,我并非100%确定它是否符合标准。

我认为使用不初始化任何成员变量的placement new,以保留以前的类状态,是C ++中未定义的行为。想象一下,有一个调试位置new,它将所有未初始化的成员变量初始化为0xCC。


在这种情况下,

union是更好的解决方案。 然而 ,您似乎正在实施strategy pattern。如果是这样,请使用strategy pattern,这将使代码更容易理解&amp;维护。

注意:使用联合时应删除virtual 如Mehrdad所述,添加它是不正确的,因为引入虚函数不符合标准布局。

example

#include <iostream>
#include <string>

using namespace std;

class Thing {
    int a;
public:
    Thing(int v = 0): a (v) {}
    const char * type_name(){ return "Thing"; }
    int value() { return a; }
};

class OtherThing : public Thing {
public:
    OtherThing(int v): Thing(v) {}

    const char * type_name() { return "Other Thing"; }
};

union Something {
    Something(int v) : t(v) {}
    Thing t;
    OtherThing ot;
};

int main() {
    Something sth{42};
    std::cout << sth.t.type_name() << "\n";
    std::cout << sth.t.value() << "\n";

    std::cout << sth.ot.type_name() << "\n";
    std::cout << sth.ot.value() << "\n";
    return 0;
}

如标准中所述:

  

在联合中,最多一个非静态数据成员可以随时处于活动状态,也就是说,任何时候最多一个非静态数据成员的值都可以存储在一个联合中。 [注意:为了简化联合的使用,我们做了一个特别的保证:如果标准布局联合包含几个共享公共初始序列(9.2)的标准布局结构,并且这个标准布局联合类型的对象包含一个标准布局结构,允许检查任何标准布局结构成员的公共初始序列;见9.2。 - 结束说明]

答案 1 :(得分:1)

(已在RSDN http://rsdn.ru/forum/cpp/5437990.1回答)

有一个棘手的方法:

struct Base
{
    int x, y, z;
    Base(int i) : x(i), y(i+i), z(i*i) {}
    virtual void whoami() { printf("%p base %d %d %d\n", this, x, y, z); }
};

struct Derived : Base
{
    Derived(Base&& b) : Base(b) {}
    virtual void whoami() { printf("%p derived %d %d %d\n", this, x, y, z); }
};

int main()
{
    Base b(3);
    Base* p = &b;

    b.whoami();
    p->whoami();

    assert(sizeof(Base)==sizeof(Derived));
    Base t(std::move(b));
    Derived* d = new(&b)Derived(std::move(t));

    printf("-----\n");
    b.whoami(); // the compiler still believes it is Base, and calls Base::whoami
    p->whoami(); // here it calls virtual function, that is, Derived::whoami
    d->whoami();
};

当然,这是UB。

答案 2 :(得分:1)

  

问题:对于解决'a'和'b'问题的动态子类化是否有更好的解决方案?

如果您有固定的子类集,那么您可以考虑使用像boost::variant这样的代数数据类型。分别存储共享数据并将所有不同的部分放入变体中。

这种方法的属性:

  • 自然适用于固定的“子类”。 (但是,某种类型擦除的类可以放入变体中,而set也可以打开)
  • 通过小型积分标签上的开关完成调度。标签的大小可以最小化为一个char。如果你的“子类”是空的 - 那么将会有一些额外的开销(取决于对齐),因为boost::variant没有执行empty-base-optimization
  • “子类”可以包含任意内部数据。来自不同“子类”的此类数据将放在一个aligned_storage
  • 您可以使用“子类”进行大量操作,每批只使用一次调度,而一般情况下,虚拟或间接调用将按调用方式进行调度。此外,从“子类”内部调用方法不会有间接,而对于虚拟调用,您应该使用final关键字来尝试实现此目的。
  • 应该明确传递
  • self基础共享数据。

好的,这是概念验证:

struct ThingData
{
    int f1;
    int f2;
};

struct Summator
{
    void doAction1(ThingData &self)  { self.f1 += self.f2; }
    const char* type_name() { return "Summator"; }
};

struct Substractor
{
    void doAction1(ThingData &self)  { self.f1 -= self.f2; }
    const char* type_name() { return "Substractor"; }
};

using Thing = SubVariant<ThingData, Summator, Substractor>;

int main()
{
    auto test = [](auto &self, auto &sub)
    {
        sub.doAction1(self);
        cout << sub.type_name() << " " << self.f1 << " " << self.f2 << endl;
    };

    Thing x = {{5, 7}, Summator{}};
    apply(test, x);
    x.sub = Substractor{};
    apply(test, x);

    cout << "size: " << sizeof(x.sub) << endl;
}

输出是:

Summator 12 7
Substractor 5 7
size: 2

LIVE DEMO on Coliru

完整代码(它使用了一些C ++ 14功能,但可以机械转换为C ++ 11):

#define BOOST_VARIANT_MINIMIZE_SIZE

#include <boost/variant.hpp>
#include <type_traits>
#include <functional>
#include <iostream>
#include <utility>

using namespace std;

/****************************************************************/
// Boost.Variant requires result_type:
template<typename T, typename F>
struct ResultType
{
     mutable F f;
     using result_type = T;

     template<typename ...Args> T operator()(Args&& ...args) const
     {
         return f(forward<Args>(args)...);
     }
};

template<typename T, typename F>
auto make_result_type(F &&f)
{
    return ResultType<T, typename decay<F>::type>{forward<F>(f)};
}
/****************************************************************/
// Proof-of-Concept
template<typename Base, typename ...Ts>
struct SubVariant
{
    Base shared_data;
    boost::variant<Ts...> sub;

    template<typename Visitor>
    friend auto apply(Visitor visitor, SubVariant &operand)
    {
        using result_type = typename common_type
        <
            decltype( visitor(shared_data, declval<Ts&>()) )...
        >::type;

        return boost::apply_visitor(make_result_type<result_type>([&](auto &x)
        {
            return visitor(operand.shared_data, x);
        }), operand.sub);
    }
};
/****************************************************************/
// Demo:

struct ThingData
{
    int f1;
    int f2;
};

struct Summator
{
    void doAction1(ThingData &self)  { self.f1 += self.f2; }
    const char* type_name() { return "Summator"; }
};

struct Substractor
{
    void doAction1(ThingData &self)  { self.f1 -= self.f2; }
    const char* type_name() { return "Substractor"; }
};

using Thing = SubVariant<ThingData, Summator, Substractor>;

int main()
{
    auto test = [](auto &self, auto &sub)
    {
        sub.doAction1(self);
        cout << sub.type_name() << " " << self.f1 << " " << self.f2 << endl;
    };

    Thing x = {{5, 7}, Summator{}};
    apply(test, x);
    x.sub = Substractor{};
    apply(test, x);

    cout << "size: " << sizeof(x.sub) << endl;
}

答案 3 :(得分:0)

使用return new(p) static_cast<TO_T&&>(*p);

这是关于移动语义的一个很好的资源:What are move semantics?

答案 4 :(得分:0)

您只是无法合法地“更改”C ++中对象的

但是,如果你提到为什么需要这个,我们可能会建议替代方案。我能想到这些:

  1. “手动”执行v表。换句话说,给定类的每个对象都应该有一个指向函数指针表的指针,该表描述了类的行为。要修改此类对象的行为,请修改函数指针。非常痛苦,但这就是v-table的重点:从你身上抽象出来。

  2. 使用有区别的联合(variant等)在同一类对象中嵌套可能不同类型的对象。我不确定这是否适合你。

  3. 执行特定于实现的内容。你可以在网上找到你所使用的任何实现的v-table格式,但是你正在进入未定义行为的领域,所以你正在玩火。它很可能不适用于其他编译器。

答案 5 :(得分:0)

您应该能够通过将数据与Thing类分开来重用数据。像这样:


template <class TData, class TBehaviourBase>
class StateStorageable {
    struct StateStorage {
        typedef typename std::aligned_storage<sizeof(TData), alignof(TData)>::type DataStorage;
        DataStorage data_storage;

        typedef typename std::aligned_storage<sizeof(TBehaviourBase), alignof(TBehaviourBase)>::type BehaviourStorage;
        BehaviourStorage behaviour_storage;

        static constexpr TData *data(TBehaviourBase * behaviour) {
            return reinterpret_cast<TData *>(
                reinterpret_cast<char *>(behaviour) -
                (offsetof(StateStorage, behaviour_storage) -
                offsetof(StateStorage, data_storage)));
        }
    };

public:
    template <class ...Args>
    static TBehaviourBase * create(Args&&... args) {
        auto storage = ::new StateStorage;

        ::new(&storage->data_storage) TData(std::forward<Args>(args)...);

        return ::new(&storage->behaviour_storage) TBehaviourBase;
    }

    static void destroy(TBehaviourBase * behaviour) {
        auto storage = reinterpret_cast<StateStorage *>(
            reinterpret_cast<char *>(behaviour) -
            offsetof(StateStorage, behaviour_storage));
        ::delete storage;
    }

protected:
    StateStorageable() = default;

    inline TData *data() {
        return StateStorage::data(static_cast<TBehaviourBase *>(this));
    }
};

struct Data {
    int a;
};

class Thing : public StateStorageable<Data, Thing> {
public:
    virtual const char * type_name(){ return "Thing"; }
    virtual int value() { return data()->a; }
};

当您将Thing更改为其他类型时,保证数据保持不变,并且应在编译时计算偏移量,以便不会影响性能。

如果属性设置为static_assert,您应该能够确保所有偏移都正确并且有足够的存储空间来存放您的类型。现在,您只需要更改创建和销毁Thing的方式。


int main() {
    Thing * thing = Thing::create(Data{42});
    std::cout << thing->type_name() << "\n";
    std::cout << thing->value() << "\n";

    turn_thing_to<OtherThing>(thing);
    std::cout << thing->type_name() << "\n";
    std::cout << thing->value() << "\n";

    Thing::destroy(thing);

    return 0;
}

由于没有重新分配thing,因此可以使用turn_thing_to

的结果修复,因此仍有UB

int main() {
    ...
    thing = turn_thing_to<OtherThing>(thing);
    ...
}

答案 6 :(得分:0)

这是另一个解决方案

虽然它略微不太理想(使用中间存储和CPU周期来调用移动ctors)但它不会改变原始任务的语义。

#include <iostream>
#include <string>
#include <memory>

using namespace std;

struct A
{
    int x;
    std::string y;
    A(int x, std::string y) : x(x), y(y) {}
    A(A&& a) : x(std::move(a.x)), y(std::move(a.y)) {}

    virtual const char* who() const { return "A"; }
    void show() const { std::cout << (void const*)this << " " << who() << " " << x << " [" << y << "]" << std::endl; }
};

struct B : A
{
    virtual const char* who() const { return "B"; }
    B(A&& a) : A(std::move(a)) {}
};

template<class TO_T> 
  inline TO_T* turn_A_to(A* a) {
    A temp(std::move(*a));
    a->~A();
    return new(a) B(std::move(temp));
  }


int main()
{
    A* pa = new A(123, "text");
    pa->show(); // 0xbfbefa58 A 123 [text]
    turn_A_to<B>(pa);
    pa->show(); // 0xbfbefa58 B 123 [text]

}

及其ideone

该解决方案源于Nickolay Merkin在下面表达的观点。 但他怀疑UB位于turn_A_to<>()

答案 7 :(得分:0)

我遇到同样的问题,虽然我没有使用它,但我想到的一个解决方案是拥有一个类,并使方法根据&#34;项类型&#34;进行切换。班上的人数。更改类型就像更改类型编号一样简单。

class OneClass {

  int iType;

  const char* Wears() {
      switch ( iType ) {
      case ClarkKent:
          return "glasses";
      case Superman:
          return "cape";
      }
  }
}

:
:

OneClass person;
person.iType = ClarkKent;
printf( "now wearing %s\n", person.Wears() );
person.iType = Superman;
printf( "now wearing %s\n", person.Wears() );