nikitas350
nikitas350

Reputation: 57

Accessing an object in operator new

My professor in C++ has shown us this as an example in overloading the operator new (which i believe is wrong):

class test {
    // code
    int *a;
    int n;
public:
    void* operator new(size_t);
};

void* test::operator new(size_t size) {
    test *p;
    p=(test*)malloc(size);
    cout << "Input the size of array = ?";
    cin >> p->n;
    p->a = new int[p->n];
    return p;
}

Is this right?

Upvotes: 0

Views: 497

Answers (6)

CB Bailey
CB Bailey

Reputation: 792537

This is very bad code because it takes initialization code that should be part of a constructor and puts it in operator new which should only allocate new memory.

The expression new test may leak memory (that allocated by p->a = new int[p->n];) and the expression new test() definitely will leak memory. There is nothing in the standard that prevents the implementation zeroing, or setting to an alternate value, the memory returned by a custom operator new before that memory is initialized with an object even if the subsequent initialization wouldn't ordinarily touch the memory again. If the test object is value-initialized the leak is guaranteed.

There is also no easy way to correctly deallocate a test allocated with new test. There is no matching operator delete so the expression delete t; will do the wrong thing global operator delete to be called on memory allocated with malloc.

Upvotes: 1

Loki Astari
Loki Astari

Reputation: 264581

This does not work.

Your professor code will fail to initialize correctly in 3/4 of cases.

  • It does not initialize objects correctly (new only affects pointers).
  • The default constructor generated for tests has two modes.
    • Zero Initialization (which happens after new, but POD are set to zero)
    • Default Initialization (POD are uninitialized)

Running Code (comments added by hand)

$ ./a.exe
Using Test::new
Using Test::new
A Count(  0)               // zero initialized:  pointer leaked.
A Pointer(0)
B Count(  10)              // Works as expected because of default init.
B Pointer(0xd20388)
C Count(  1628884611)      // Uninitialized as new not used.
C Pointer(0x611f0108)
D Count(  0)               // Zero initialized because it is global (static storage duration)
D Pointer(0)

The Code

#include <new>
#include <iostream>
#include <stdlib.h>

class test
{
    // code
        int *a;
        int n;
    public:
        void* operator new(size_t);

        // Added dredded getter so we can print the values. (Quick Hack).
        int* getA() const { return a;}
        int  getN() const { return n;}
};

void* test::operator new(size_t size)
{
    std::cout << "Using Test::new\n";
    test *p;
    p=(test*)malloc(size);
    p->n = 10;             // Fixed size for simple test.
    p->a = new int[p->n];
    return p;
}

// Objects that have static storage duration are zero initialized.
// So here 'a' and 'n' will be set to 0
test d;

int main()
{
    // Here a is zero initialized. Resulting in a and n being reset to 0
    // Thus you have memory leaks as the reset happens after new has completed.
    test* a = new test();
    // Here b is default initialized.
    // So the POD values are undefined (so the results are what you prof expects).
    // But the standard does not gurantee this (though it will usually work because
    // of the it should work as a side effect of the 'zero cost principle`)
    test* b = new test;

    // Here is a normal object.
    // New is not called so its members are random.
    test  c;

    // Print out values
    std::cout << "A Count(  " << a->getN() << ")\n";
    std::cout << "A Pointer(" << a->getA() << ")\n";
    std::cout << "B Count(  " << b->getN() << ")\n";
    std::cout << "B Pointer(" << b->getA() << ")\n";
    std::cout << "C Count(  " << c.getN() << ")\n";
    std::cout << "C Pointer(" << c.getA() << ")\n";
    std::cout << "D Count(  " << d.getN() << ")\n";
    std::cout << "D Pointer(" << d.getA() << ")\n";
}

A valid example of what the professor failed to do:

class test
{
    // code
        int n;
        int a[1];  // Notice the zero sized array.
                   // The new will allocate enough memory for n locations.
    public:
        void* operator new(size_t);

        // Added dredded getter so we can print the values. (Quick Hack).
        int* getA() const { return a;}
        int  getN() const { return n;}
};

void* test::operator new(size_t size)
{
    std::cout << "Using Test::new\n";

    int  tmp;
    std::cout << How big?\n";
    std::cin  >> tmp;

    // This is a half arsed trick from the C days.
    // It should probably still work.
    // Note: This may be what the professor should have wrote (if he was using C)
    //       This is totally horrible and whould not be used.
    //       std::vector is a much:much:much better solution.
    //       If anybody tries to convince you that an array is faster than a vector
    //       The please read the linked question below where that myth is nailed into
    //       its over sized coffin.
    test *p =(test*)malloc(size + sizeof(int) * tmp);
    p->n = tmp;
    // p->a  = You can now overflow a upto n places.
    return p;
}

Is std::vector so much slower than plain arrays?

Upvotes: 0

UncleBens
UncleBens

Reputation: 41351

Your professor is completely misunderstanding the purpose of operator new whose only task is to allocate as much memory as was asked and to return a void* to it.

After that the constructor is called to initialize the object at that memory location. This is not up to the programmer to avoid.

As the class doesn't have a user-defined constructor, the fields are supposed to be uninitialized, and in such a case the compiler has probably freedom to initialize them to some magic value in order to help finding use of uninitialized values (e.g for debug builds). That would defeat the extra work done by the overloaded operator.

Another case where the extra work will be wasted is when using value-initialization: new test();

Upvotes: 1

Steve Jessop
Steve Jessop

Reputation: 279325

It's definitely "not right", in the sense that it's giving me the creeps.

Since test has no user-declared constructors, I think it could work provided that the instance of test isn't value-initialized (which would clear the pointer). And provided that you write the corresponding operator delete.

It's clearly a silly example, though - user interaction inside an overloaded operator new? And what if an instance of test is created on the stack? Or copied? Or created with test *tp = new test(); in C++03? Or placement new? Hardly user-friendly.

It's constructors which must be used to establish class invariants (such as "I have an array to use"), because that's the only way to cover all those cases. So allocating an array like that is the kind of thing that should be done in a constructor, not in operator new. Or better yet, use a vector instead.

As far as the standard is concerned - I think that since the class is non-POD the implementation is allowed to scribble all over the data in between calling operator new and returning it to the user, so this is not guaranteed to work even when used carefully. I'm not entirely sure, though. Conceivably your professor has run it (perhaps many years ago when he first wrote the course), and if so it worked on his machine. There's no obvious reason why an implementation would want to do anything to the memory in the specific case of this class.

I believe that is "wrong" because he access the object before the constructor.

I think you're correct on this point too - casting the pointer returned from malloc to test* and accessing members is UB, since the class test is non-POD (because it has private non-static data members) and the memory does not contain a constructed instance of the class. Again, though, there's no reason I can immediately think of why an implementation would want to do anything that stops it working, so I'm not surprised if in practice it stores the intended value in the intended location on my machine.

Upvotes: 2

sbi
sbi

Reputation: 224129

The creation/destruction of objects in C++ is divided into two tasks: memory allocation/deallocation and object initialization/deinitialization. Memory allocation/deallocation is done very differently depending on an object's storage class (automatic, static, dynamic), object initialization/deinitialization is done using the object's type's constructor/destructor.

You can customize object initialization/deinitialization by providing your own constructors/destructor. You can customize the allocation of dynamically allocated objects by overloading operator new and operator delete for this type. You can provide different versions of these operators for single objects and arrays (plus any number of additional overloads).

When you want to fine-tune the construction/destruction of objects of a specific type you first need to decide whether you want to fiddle with allocation/deallocation (of dynamically allocated objects) or with initialization/deinitialization. Your code mixes the two, violating one of C++' most fundamental design principle, all established praxis, every known C++ coding standard on this planet, and your fellow-workers' assumptions.

Upvotes: 1

aschepler
aschepler

Reputation: 72431

Did some Standard checking. Since test has private non-static members, it is not POD. So new test default-initializes the object, and new test() value-initializes it. As others have pointed out, value-initialization sets members to zero, which could come as a surprise here.

Default-initialization uses the implicitly defined default constructor, which omits initializers for members a and n.

12.6.2p4: After the call to a constructor for class X has completed, if a member of X is neither specified in the constructor's mem-initializers, nor default-initialized, nor value-initialized, nor given a value during execution of the body of the constructor, the member has indeterminate value.

Not "the value its memory had before the constructor, which is usually indeterminate." The Standard directly says the members have indeterminate value if the constructor doesn't do anything about them.

So given test* p = new test;, p->a and p->n have indeterminate value and any rvalue use of them results in Undefined Behavior.

Upvotes: 2

Related Questions