c++memorynullconstantsdynamic-memory-allocation

How to default initialise non-POD object onto same address?


tl;dr; How can one make every instance of object have same constant address, and then assign it after user wants to change it?

I have a need to develop my own NULL (ish) objects, and would like to default initialize to NullFoo (in value and in address) because each time object is constructed it just makes NullFoo but on a different address.

Within context Null (ish) value has no importance, and its there to form information management for serialization (- ie its not serialized). It is still very useful in dynamic memory management to have each 'not filled' data structure start from somewhere without wasting space.

So consider the example code bellow:

//pretty lights
#define CLOG(x) std::clog <<  "\033[48;2;100;150;100m" << "created\t" << x << "\033[m\n";
#define cLOG(x) std::clog <<  "\033[48;2;100;150;100m" << "copied\t" << x << "\033[m\n";
#define rLOG(x) std::clog <<  "\033[48;2;100;150;150m" << "reseted\t" << x << "\033[m\n";
#define dLOG(x) std::clog <<  "\033[48;2;150;100;100m" << "deleted\t" << x << "\033[m\n";
#define aLOG(x) std::clog <<  "\033[48;2;100;130;100m" << "assigned\t" << x << "\033[m\n";
#define caLOG(x) std::clog <<  "\033[48;2;100;130;120m" << "copy assigned\t" << x << "\033[m\n";

struct foo{
    int objdata;
    
    foo()
    {
        this->objdata = 0;
        CLOG(*this);
    }
    foo(const foo &other)
    {
        this->objdata = other.objdata;
        cLOG(*this);
    }
    foo(foo &&other)
    {
        this->objdata = other.objdata;
        cLOG(*this);
        other.objdata =0;
        rLOG(other);
    }
    ~foo()
    {
         dLOG(*this)
    };
    
    foo &operator=(int value)
    { 
        this->objdata = value;
        aLOG(*this);
        return *this;
    }
    foo &operator=(const foo &other)
    { 
        this->objdata =other.objdata;
        caLOG(*this);
        return *this;
    }
    foo &operator=(foo &&other)
    { 
        this->objdata = other.objdata;
        caLOG(*this);
        other.objdata = 0;
        rLOG(other);
        return *this;
    }

    //required for pre-processor shenanigans (just visual que)
    friend std::ostream &operator<<(std::ostream &, const foo &);
};


const foo NullFoo;

std::ostream &operator<<(std::ostream &os, const foo &f){
    os << f.objdata<< '\t' << &f << '\t' << &NullFoo;
    return os;
}

and entry point:

int main(){
    { // scope so pretty lights show
    std::cout << NullFoo << std::endl;
    foo a;
    std::cout << a << std::endl;
    a = rand()%0x100;
    std::cout << a << std::endl;
    }

    return 1;
}

Output:

created 0   0x5600e97b13ec  0x5600e97b13ec
0   0x5600e97b13ec  0x5600e97b13ec
created 0   0x7fff9ae33454  0x5600e97b13ec
0   0x7fff9ae33454  0x5600e97b13ec
assigned    103 0x7fff9ae33454  0x5600e97b13ec
103 0x7fff9ae33454  0x5600e97b13ec
deleted 103 0x7fff9ae33454  0x5600e97b13ec
deleted 0   0x5600e97b13ec  0x5600e97b13ec

As you can see, eac initialisation is on new address, that is subsequently destroyed. NullFoo is made on 0x5600e97b13ec and each consequent constructor makes a new address (in this case 0x7fff9ae33454). It seems that memory is allocated before constructor is even called (but i don't know how to check that).

How can I make every instance of object have same constant address, and then assign it after user wants to change it?

Working on Ubuntu 22.04 LTS, g++ 11.4.0.


Solution

  • Two objects cannot share the same address. And one object cannot change address.

    To have something start with the same identical object and change with mutation... that is reference semantics and you get that with a (raw or smart) pointer type as described in the comments.

    For example,

    const foo NullFoo;
    // becomes -->
    const std::shared_ptr<const foo> NullFoo = std::make_shared<foo>();
    // or
    const foo NullFooObj;
    const foo* const NullFoo = &NullFooObj;
    
    foo a;
    // becomes -->
    std::shared_ptr<const foo> a = NullFoo;
    // or
    const foo* a = NullFoo;
    
    a = rand()%0x100;
    // becomes -->
    a = std::make_shared<foo>(rand()%0x100);
    // or
    foo aobj = rand()%0x100;
    a = &aobj;
    

    One can encapsulate this in an wrapper type. To use a raw pointer requires an object with a compatible lifetime, whereas a shared_ptr does not but requires the heap. A raw pointer will often be too inflexible to use for a generic API, because the caller must ensure the lifetime which means all the work must be done in one shot (no queueing, asynchrony). So, in a case where address (object identity) is important, you would probably see shared_ptr and required heap allocation.

    Within context Null (ish) value has no importance, and its there to form information management for serialization (- ie its not serialized). It is still very useful in dynamic memory management to have each 'not filled' data structure start from somewhere without wasting space.

    What you are asking for something that is a poor fit for this use case. In the code in the question, no memory allocation or heap usage occurs. Every foo is either a static object or on the stack. Trying to identify a null object by a particular address is forcing a memory management strategy that is far less optimal.

    A more efficient method to solve this problem would be to use a type that can represent the nullity:

    foo a;
    // becomes -->
    std::optional<foo> a;
    
    // this is unchanged
    a = rand()%0x100;
    

    It is true that there is technically space used to hold the foo, on the stack. However, the allocation is orders of magnitude cheaper than heap allocation.

    Alternatively, one can decide not to serialize based on certain type properties: e.g. do not serialize values equal to the value-initialized object of the same type, or do not serialize values equal to a value specified by a trait type.