Jump to content
  • Advertisement
Sign in to follow this  
Gage64

[C++] Automatic generation of create()/destroy() functions

This topic is 3614 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Every now and then I like to post stuff like this just to see it get torn apart. So what do we have this time? This is a class that enables you to have control over when an object is created and destroyed without using dynamic memory allocation. For example, if you have an object that doesn't have a default constructor, you can't have an instance of it as a class data member. You have to use a pointer along with new/delete (or just new, if you're using a smart pointer). The class is called Holder (not a great name, I know). Here's the implementation: Holder.h
#ifndef HOLDER_H
#define HOLDER_H

#include <cassert>


// Note: Currently supports objects with zero, one or two parameter c'tors
template <class T>
class Holder {
public:
    Holder():created(false)
    {
    }


    ~Holder() {
        destroy();
    }


    Holder(const Holder<T> &other) {
        create(other);
    }


    Holder<T> & operator=(const Holder<T> &other) {
        if (this == &other) {
            return *this;
        }

        // Not sure if the asserts are a good idea...
        assert(created);
        assert(other.created);

        getObj() = other.getObj();
        return *this;
    }

    
    void destroy() {
        if (created) {
            created = false;
            getObj().~T();
        }
    }


    T* operator->() {
        assert(created);
        return &getObj();
    }


    const T* operator->() const {
        assert(created);
        return &getObj();
    }


    T & operator*() {
        assert(created);
        return getObj();
    }


    const T & operator*() const {
        assert(created);
        return getObj();
    }
    
    
    void create() {
        new (obj) T;
        created = true;
    }


    template <class P1>
    void create(const P1 &p1) {
        new (obj) T(p1);
        created = true;
    }


    template <class P1, class P2>
    void create(const P1 &p1, const P2 &p2) {
        new (obj) T(p1, p2);
        created = true;
    }

private:
    T& getObj() {
        return *(reinterpret_cast<T*>(obj));
    }


    const T& getObj() const {
        return (const_cast<Holder<T>*>(this))->getObj();
    }

    char obj[sizeof(T)];
    bool created;
};


#endif  // HOLDER_H
Here's a little test:
#include "Holder.h"

#include <iostream>
#include <string>
using namespace std;


struct Student {
    string name;
    int grade;

    Student(const string &name, int grade)
        : name(name), grade(grade)
    {
        cout << "Student::Student() for " << name << endl;
    }


    ~Student() {
        cout << "Student::~Student() for " << name << endl;
    }


    void print() const {
        cout << name << ", " << grade << endl;
    }
};


void main() {
    Holder<Student> st1;
    Holder<Student> st2;

    cout << "Hello" << endl;

    st1.create("John", 87);
    st2.create("Mike", 92);

    st1->print();
    (*st2).print();
    st2 = st1;
    st2->print();

    st1.destroy();
    st1.destroy();  // Does nothing

    // No call to st2.destroy();

    cout << "Goodbye" << endl;
}
I'd like to hear your comments about the usefulness and correctness of this class. I'm just posting this out of boredom, so feel free to be as brutal as you like.

Share this post


Link to post
Share on other sites
Advertisement
Quote:

For example, if you have an object that doesn't have a default constructor, you can't have an instance of it as a class data member. You have to use a pointer along with new/delete (or just new, if you're using a smart pointer).

That sounds like your motivation for making this "holder". And it doesn't sound right at all. You just use the initializer lists (you already use them in your code) to init the object in question. This is perfectly legal. And 'A' has no default constructor.


class A
{
public:
A(int,int){}

};

class B
{
public:
B():a(1,1){}
private:
A a;
};

...
B b;





Quote:

st1.destroy();
st1.destroy(); // Does nothing

// No call to st2.destroy();

Code like that is just begging for errors. Why not destroy st2? how would I know that? how can i keep track of that? Someone WILL forget at somepoint what the code is supposed to do, ESPECIALLY when someone just copy pastes it.


Containers are tricky. Holder<myClass> will break if I overload operator & to do something other than return the address of the object in question.



Overall, I'm sorry I just don't see the usefulness of this "holder" class at all.

Share this post


Link to post
Share on other sites
Your copying semantics are very, very strange - I wouldn't recommend using this as is. However, what does you holder class offer which cannot be matched by std::auto_ptr (or std::tr1::shared_ptr, if auto_ptr isn't enough)?

Share this post


Link to post
Share on other sites
Quote:
Original post by KulSeran
That sounds like your motivation for making this "holder". And it doesn't sound right at all. You just use the initializer lists


I meant something like this:


class A {
public:
A(int) {}
};


class B {
public:
B() {
cout << "Enter data" << endl;
int data;
cin >> data;
a(data); // Obviously doesn't compile
}

private:
A a;
;}



Basically it's for cases where you must perform some set of operations before creating the object.

Quote:
Quote:

st1.destroy();
st1.destroy(); // Does nothing

// No call to st2.destroy();

Code like that is just begging for errors. Why not destroy st2?


This is just to ensure that the object will be destructed, even if you forget to call destroy(). Another option might be to trigger an assert in the destructor, but I don't know if that's a good idea.

Share this post


Link to post
Share on other sites
The life time of objects in C++ is defined precisely. An object that is successfully created should be an operational object, or shouldn't have been created at all (which mostly translate into: if the execution reaches a point past the creation of an object, then the object exists. Otherwise an exception have been thrown).

Your system allows an object to be in a state where it is created, only it really isn't. That's an horrible thing to do.

One very powerful programming pattern in c++ (which isn't possible with the other contemporary popular languages) is RAII, explained there: http://www.hackcraft.net/raii/

This doesn't work when you muddy up the object life duration semantics with additional initialization and destruction methods.

Doing such things is one of several reasons why MFC sucked.

Share this post


Link to post
Share on other sites
Quote:
Original post by KulSeran
Containers are tricky. Holder<myClass> will break if I overload operator & to do something other than return the address of the object in question.


I'm not sure what you mean, but isn't that true for any class? For example with code like this:


class A {
public:
const A & operator=(const A &other) {
if (this == &other) // <--
// ...
}
};


Quote:
Original post by swiftcoder
Your copying semantics are very, very strange - I wouldn't recommend using this as is.


Yeah, I wasn't sure how to handle that...

Quote:
However, what does you holder class offer which cannot be matched by std::auto_ptr (or std::tr1::shared_ptr, if auto_ptr isn't enough)?


Like I said, it doesn't use dynamic memory allocation.

Quote:
Original post by Zlodo
Your system allows an object to be in a state where it is created, only it really isn't. That's an horrible thing to do.


I agree, but I tried to make sure that if you use the holder before properly creating it, an assertion will be triggered.

Quote:
One very powerful programming pattern in c++ (which isn't possible with the other contemporary popular languages) is RAII, explained there: http://www.hackcraft.net/raii/

This doesn't work when you muddy up the object life duration semantics with additional initialization and destruction methods.


Well, the holder will destruct its internal object even if you don't call destroy(), so in that sense it's kind of like RAII... Or did I misunderstand you?

Share this post


Link to post
Share on other sites
Quote:

class A {
public:
A(int) {}
};


class B {
public:
B() {
cout << "Enter data" << endl;
int data;
cin >> data;
a(data); // Obviously doesn't compile
}

private:
A a;
;}


OBJECT CREATION is NOT a place for DATA INPUT.
Sorry. But that is REALLY bad practice right there. You should be inputing data outside the constructor.
and even if you don't fix that, most objects that don't have a default constructor have reasons for it. You should work with it, not against it.

Share this post


Link to post
Share on other sites
Quote:
Original post by KulSeran
OBJECT CREATION is NOT a place for DATA INPUT.
Sorry. But that is REALLY bad practice right there. You should be inputing data outside the constructor.
and even if you don't fix that, most objects that don't have a default constructor have reasons for it. You should work with it, not against it.


Maybe it was a bad example, but like I said, this is for situations where you have to create the object after doing several operations so you can't use the initializer list. In fact, my holder allows you to do this for objects that don't have default constructors.

BTW, although it's unrelated, could you please explain why you shouldn't input data inside a constructor?

Share this post


Link to post
Share on other sites
Quote:
Original post by Gage64
Well, the holder will destruct its internal object even if you don't call destroy(), so in that sense it's kind of like RAII... Or did I misunderstand you?


What happens if, after you instantiate the object but before you initialize it, you encounter an error condition and need to exit the function, or an exception is thrown?

(oops, didn't notice the created flag.)

Share this post


Link to post
Share on other sites
I think the RAII post is more to the point of:
I have a mutex assistance class called ScopeLock.
Lock doesn't have a default constructor, you have to call ScopeLock(mutex).
The scoping rules for C++ insure once it is created it holds the mutex and once it dies the mutex is released.
If you get fancy and do "new ScopeLock(mutex)" you atleast know you have to smart pointer or manage the destruction.

{
Holder<ScopeLock> L;
// ... A
{
L.create(mutex);
// ... B
}
// ... C
}




Now, tell me it is 100% clear where ScopeLock's scope begins and ends.
Is A locked? B? C? This is a coding nightmare.
Besides, you lose (same with new ScopeLock(mutex)) the auto-magic of knowing the lock is released no mater how the function ends up leaving the scope level it was created for.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!