Andrew Tomazos
Andrew Tomazos

Reputation: 68698

Adding C++11 object to list when dynamically allocated?

Suppose I have a class X:

struct X
{
   ...
};

And I have a global vector V:

vector<X*> V;

I want to add a new instance of X to V if and only if it is dynamically allocated (as a complete most derived object, not a subobject):

int main()
{
    X x; // not added to V
    new X; // added to V

    struct D : X {};
    new D; // not added to V
}

Is there some way to do this? Perhaps by overloading/overriding operator new somehow?

Upvotes: 9

Views: 375

Answers (3)

dyp
dyp

Reputation: 39121

Building on aschepler's approach, but now using a virtual base class to redirect D's constructor calls (to not add an instance to the vector).

Main idea is to do a two-step registration: first, register any call to operator new (whether it's for X or a derived class) to an unordered_set (X::dyn_alloc_set). Then, when constructing X, selecting based upon the most-derived type, add this to V if it has been dynamically allocated and if it's not a derived class.

The constructor of a virtual base class has to be called from the most-derived type, therefore you can use it to differentiate between D and X during construction.

#include <unordered_set>
#include <typeinfo>
#include <vector>
#include <iostream>
#include <algorithm>

struct X;
std::vector<X*> V;

struct virt_base_class
{
    friend struct X;
private:
    virt_base_class(X* p);  // this can only and will only be called by X
public:
    virt_base_class()  // this will be called by any class derived from X
    {}
};

struct X
    : protected virtual virt_base_class
{
private:
    friend class virt_base_class;
    static std::unordered_set<X*> dyn_alloc_set;
    static bool dynamically_allocated(X* p)
    {
        return dyn_alloc_set.count(p) > 0;
    }    

public:
    X()
        : virt_base_class(this)
    {}

    static void* operator new(std::size_t size) {
        void* p = ::operator new(size);
        if (size == sizeof(X))
            dyn_alloc_set.insert( static_cast<X*>(p) );
        return p;
    }
    static void operator delete(void* p, std::size_t size) {
        if (size == sizeof(X))
        {
            dyn_alloc_set.erase( static_cast<X*>(p) );
            V.erase( std::remove(V.begin(), V.end(), static_cast<X*>(p)),
                     V.end() );
        }
        ::operator delete(p);
    }
};


virt_base_class::virt_base_class(X* p)
{
    if( X::dynamically_allocated(p) )
        V.push_back(p);
}

struct D : X
{};  // D::D will implicitly call virt_base_class::virt_base_class()


std::unordered_set<X*> X::dyn_alloc_set;


int main()
{
    X x;
    X* p = new X;
    D d;
    D* pd = new D;

    std::cout << V.size();
}

Update: using thread_local storage to avoid the unordered_set:

struct X
    : protected virtual virt_base_class
{
private:
    friend class virt_base_class;
    static thread_local X* last_dyn_allocated;
    static bool dynamically_allocated(X* p)
    {
        return p == last_dyn_allocated;
    }

public:
    X()
        : virt_base_class(this)
    {}

    static void* operator new(std::size_t size) {
        void* p = ::operator new(size);
        if (size == sizeof(X))
        {
            last_dyn_allocated = static_cast<X*>(p);
        }
        return p;
    }
    static void operator delete(void* p, std::size_t size) {
        if (size == sizeof(X))
        {
            X* pp = static_cast<X*>(p);
            if(last_dyn_allocated == pp)
                last_dyn_allocated = nullptr;

            V.erase( std::remove(V.begin(), V.end(), pp),
                     V.end() );
        }
        ::operator delete(p);
    }
};

thread_local X* last_dyn_allocated = nullptr;

Upvotes: 6

Casey
Casey

Reputation: 42554

I think the best approximation to what you want is to force the use of a factory for dynamically allocated objects:

#include <algorithm>
#include <utility>
#include <vector>

class X {
  static std::vector<X*> dynamic_xs;

  static void* operator new(std::size_t size) {
    return ::operator new(size);
  }

public:
  ~X() {
    auto end = std::end(dynamic_xs);
    auto pos = std::find(std::begin(dynamic_xs), end, this);
    if (pos != end) {
      if (pos != --end) {
        std::iter_swap(pos, end - 1);
      }
      dynamic_xs.pop_back();
    }
  }

  template <typename... Args>
  friend X* make_x(Args&&... args) {
    X* p = new X(std::forward<Args>(args)...);
    dynamic_xs.push_back(p);
    return p;
  }
};

std::vector<X*> X::dynamic_xs;

Clients can instantiate stack-allocated X, but dynamic allocation will cause an access error since operator new is private.

Upvotes: 1

aschepler
aschepler

Reputation: 72421

struct X {
public:
    static void* operator new(std::size_t size) {
        void* p = ::operator new(size);
        if (size == sizeof(X))
            V.push_back(static_cast<X*>(p));
    }
    static void operator delete(void* p, std::size_t size) {
        if (size == sizeof(X))
            V.erase(std::remove(V.begin(), V.end(), p), V.end());
        ::operator delete(p, size);
    }
};

Note that there will be times when elements of V point at memory that is not yet or no longer actually an X. It's possible for users to get around these functions, but they'd have to be trying.

If you have another class that inherits X but has the same size (so no other subobjects except maybe "empty" base classes), like struct Y : public X {};, the code above will think new Y is allocating an X. If this is an issue, you'd need to also add operator new and operator void to every such class Y. I don't think there's a more general solution.

Upvotes: 8

Related Questions