8 replaced http://stackoverflow.com/ with https://stackoverflow.com/
source | link

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028https://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

The STL isn't enough for engines. https://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

7 Rollback to Revision 5
source | link

Private messagesSo the other answer is good, but I will say this. In C++ the RAII idiom means "you never type object.close() or object.free() or object.release() or anything like that, it is a bit laxer on initialisation because of assignment operators.

Suppose we have the following

class Object {
public:
    Object() { /* CANNOT doStuff after this, as it isn't ready*/ }
    Object& operator=(const Object&);
    ~Object() { /*code to tidy up*/ }

    void load(Whatever);
    void doStuff();

private:
    /*implementation*/
};

I can now do stuff like:

Object o1; //not valid here
Object o2; //careful - you cannot use virtuals in a constructor!
o2.load(stuff); //o2 is now valid
o1=o2; //copy-assign, both valid

When this sitecode-block ends both will automatically be destructed and clean up.

The important part is that the destructor clears up. This means if you have the following:

Asset::~Asset() {
    if(isLoaded()) {
        unload();
    }
}

Now your destructor will unload stuff - if it is loaded.

What you need is a way to communicate "you cannot use an asset that hasn't been loaded" without exceptions (as exceptions degrade performance quite severely)

If you use this then structures filled with assets will automatically unload when you destroy them, as it should be!


Exemplary uses of RAII

One of my all time favourite C++ things is:

1) code.whatever();
2) {Guard g(resource);
3)     do.this.that(BOTH).with(resource);
4) }
5) more.soForth();

g will be destroyed "at line 4" (proper scoping) thus automatically releasing the lock on resource it holds.


Game engines don't count

Note also that with game engines we care about performance NOT nice code. We're not systems programmers. A good example of this is when you have a virtual function you might need to call loads of times. It is far faster to have some sort of "type ID" and force a cast to the correct type then jump (without devirtualising) to something than it is to do exist BTWthe virtual call.

This is a total anti-pattern and if I saw a systems programmer doing it I'd probably break their legs. It's okay for us chosen few!


Why you need "invalid" things

Vectors and arrays are cheep. Even if you're going for maximum performance that default constructor running isn't going to be the bottleneck (letting the compiler see it in the header file will help as then it can inline it and wont have to do a full-on function call) and premature optimisation is evil.

Suppose you have a MyVector<T> which is actually a T[] underneath it all, which you get by doing data=new T[size] - this isn't bad to start with (even though it involves size calls to T::T()), the biggest problem here will be memory allocation (new becomes the enemy) - there is literally no need to optimise this now.

Now when you construct your T[] you need to have a default constructor (linked lists can get away without this as they construct new nodes as needed) and this is "classic C++" in the sense of "I know that the array is initialised" and I encourage you to do this even though it is "not as fast". If this is the bottleneck you can of course use malloc and inplace new everything you need. You just have to be very careful, so worry about this later.

NOTE ABOUT PITFALLS
If you change your MyVector implementation from something like:

  • data[size+1] = incoming; //will use T::operator= To

  • new(data+size+1) (incoming) //will use T::T

Which may require some tweaking if one of your T has an operator= for an obscure class but no constructor. Otherwise nothing will change.

OTHER PITFALL
This one is often missed, right now you have this magic global new - if that goes you'll require an Allocator& to everything that might have to allocate. Keep this in mind. If you do switch to a faster more game-friendly allocator, how will your objects get hold of a reference for it?


Life without exceptions

In theory stuff like thing.use(invalidAsset) should never happen, and usually when it does you'd have an exception.

Exceptions are the enemy in the engine, this is why you may find in other projects nothing seems to return a T& only T* - this is because you can test a T* for being a nullptr. Like with exceptions though, no one seems to bother.

Anyway, you should ask "how can I make debugging these easier?" the secret is the pre-processor.

Somewhere you need a config file and in it you shall write:

#define MYPROJ_SANITY_CHECK            1
#define MYPROJ_SANITY_CHECK_ASSETS     MYPROJ_SANITY_CHECK

and another file where you define this:

#define MYPROJ_EXIT(MSG) ::MyProject::exit(MSG,__LINE__,__FILE__);
namespace MyProject {
    exit(const char* message, int line, const char* file);
}

and make sure the implementation of exit outputs something on stderr and EXITS, not exceptions, insta-crash it! (Great place to set breakpoints!)

Then when you use an Asset (this is the hard part) you turn this:

use(incomingAsset);

into

#if MYPROJ_SANITY_CHECK_ASSETS == 1
    if(!incomingAsset->isLoaded()) {
        MYPROJ_EXIT("Not-loaded asset passed, loaded expected")
    }
#endif
use(incomingAsset)

This way you still get to do checks (which you want!) but you can turn them all off easily (which you also want!)


Real example

template<class T> class DeleteWhenDone;
namespace std {
template<class T>
void swap(DeleteWhenDone<T>&,DeleteWhenDone<T>&) noexcept;
}

template<class T>
class DeleteWhenDone {
public:
    DeleteWhenDone() { data = nullptr; }
    DeleteWhenDone(T* source): data(source) { }
    DeleteWhenDone(const DeleteWhenDone&) = delete;
    DeleteWhenDone(DeleteWhenDone&& from) noexcept: data(from.data) { from.data = nullptr; }
    DeleteWhenDone& operator=(const DeleteWhenDone&) = delete;
    DeleteWhenDone& operator=(DeleteWhenDone&& from) {
        DeleteWhenDone tmp(::std::forward<DeleteWhenDone>(from));
        ::std::swap(*this,tmp);
        return *this;
    }
    ~DeleteWhenDone() { delete data; }
private:
    T* data;

friend void ::std::swap<>(DeleteWhenDone&,DeleteWhenDone&) noexcept;
};
namespace std {

template<class T>
void swap(::DeleteWhenDone<T>& A, ::DeleteWhenDone<T>& B) noexcept {
    T* tmp = A.data;
    A.data = B.data;
    B.data = tmp;
}

}

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

Notice the default constructor and the way I implement the move operators, this is very common in systems programming and the compiler will optimise it.

I use this example to illustrate something unexpected. Notice I use nullptr as my "not initialised" value. Compiling with -O3 -fno-exceptions with the following test program

DeleteWhenDone<int> getInt() {
    DeleteWhenDone<int> a(new int(5));
    return a;
}
int main() {
    DeleteWhenDone<int> test;
    test = getInt();
}

we get the following code

0000000000400670 <main>:
  400670:       53                      push   %rbx
  400671:       bf 04 00 00 00          mov    $0x4,%edi
  400676:       e8 e5 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
  40067b:       31 ff                   xor    %edi,%edi
  40067d:       c7 00 05 00 00 00       movl   $0x5,(%rax)
  400683:       48 89 c3                mov    %rax,%rbx
  400686:       e8 85 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40068b:       31 ff                   xor    %edi,%edi
  40068d:       e8 7e ff ff ff          callq  400610 <operator delete(void*)@plt>
  400692:       48 89 df                mov    %rbx,%rdi
  400695:       e8 76 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40069a:       31 c0                   xor    %eax,%eax
  40069c:       5b                      pop    %rbx
  40069d:       c3                      retq   
  40069e:       66 90                   xchg   %ax,%ax

3 calls to delete!! This is BAD (although to a systems programmer this is great)

Earlier when I put the "checking for loadedness in a destructor" I meant it. If we change the destructor to:

    ~DeleteWhenDone() { if(data != nullptr) { delete data; } }

We get the following:

    0000000000400670 <main>:
      400670:       48 83 ec 08             sub    $0x8,%rsp
      400674:       bf 04 00 00 00          mov    $0x4,%edi
      400679:       e8 e2 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
      40067e:       c7 00 05 00 00 00       movl   $0x5,(%rax)
      400684:       48 89 c7                mov    %rax,%rdi
      400687:       e8 84 ff ff ff          callq  400610 <operator delete(void*)@plt>
      40068c:       31 c0                   xor    %eax,%eax
      40068e:       48 83 c4 08             add    $0x8,%rsp
      400692:       c3                      retq   
      400693:       66 66 66 66 2e 0f 1f    data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
      40069a:       84 00 00 00 00 00 

The last 2 lines are some sort of boundary padding.

The sub at the start moves the stack pointer back 8 bytes (a pointer size) so that the call to operator new returns the result exactly where it'll be needed (on main's stack). At the end it adds it again so when main returns the stack is as expected. You'll see how "truly optimum" this is.

This is the kind of stuff you need to be mindful of.

Private messages on this site do exist BTW

So the other answer is good, but I will say this. In C++ the RAII idiom means "you never type object.close() or object.free() or object.release() or anything like that, it is a bit laxer on initialisation because of assignment operators.

Suppose we have the following

class Object {
public:
    Object() { /* CANNOT doStuff after this, as it isn't ready*/ }
    Object& operator=(const Object&);
    ~Object() { /*code to tidy up*/ }

    void load(Whatever);
    void doStuff();

private:
    /*implementation*/
};

I can now do stuff like:

Object o1; //not valid here
Object o2; //careful - you cannot use virtuals in a constructor!
o2.load(stuff); //o2 is now valid
o1=o2; //copy-assign, both valid

When this code-block ends both will automatically be destructed and clean up.

The important part is that the destructor clears up. This means if you have the following:

Asset::~Asset() {
    if(isLoaded()) {
        unload();
    }
}

Now your destructor will unload stuff - if it is loaded.

What you need is a way to communicate "you cannot use an asset that hasn't been loaded" without exceptions (as exceptions degrade performance quite severely)

If you use this then structures filled with assets will automatically unload when you destroy them, as it should be!


Exemplary uses of RAII

One of my all time favourite C++ things is:

1) code.whatever();
2) {Guard g(resource);
3)     do.this.that(BOTH).with(resource);
4) }
5) more.soForth();

g will be destroyed "at line 4" (proper scoping) thus automatically releasing the lock on resource it holds.


Game engines don't count

Note also that with game engines we care about performance NOT nice code. We're not systems programmers. A good example of this is when you have a virtual function you might need to call loads of times. It is far faster to have some sort of "type ID" and force a cast to the correct type then jump (without devirtualising) to something than it is to do the virtual call.

This is a total anti-pattern and if I saw a systems programmer doing it I'd probably break their legs. It's okay for us chosen few!


Why you need "invalid" things

Vectors and arrays are cheep. Even if you're going for maximum performance that default constructor running isn't going to be the bottleneck (letting the compiler see it in the header file will help as then it can inline it and wont have to do a full-on function call) and premature optimisation is evil.

Suppose you have a MyVector<T> which is actually a T[] underneath it all, which you get by doing data=new T[size] - this isn't bad to start with (even though it involves size calls to T::T()), the biggest problem here will be memory allocation (new becomes the enemy) - there is literally no need to optimise this now.

Now when you construct your T[] you need to have a default constructor (linked lists can get away without this as they construct new nodes as needed) and this is "classic C++" in the sense of "I know that the array is initialised" and I encourage you to do this even though it is "not as fast". If this is the bottleneck you can of course use malloc and inplace new everything you need. You just have to be very careful, so worry about this later.

NOTE ABOUT PITFALLS
If you change your MyVector implementation from something like:

  • data[size+1] = incoming; //will use T::operator= To

  • new(data+size+1) (incoming) //will use T::T

Which may require some tweaking if one of your T has an operator= for an obscure class but no constructor. Otherwise nothing will change.

OTHER PITFALL
This one is often missed, right now you have this magic global new - if that goes you'll require an Allocator& to everything that might have to allocate. Keep this in mind. If you do switch to a faster more game-friendly allocator, how will your objects get hold of a reference for it?


Life without exceptions

In theory stuff like thing.use(invalidAsset) should never happen, and usually when it does you'd have an exception.

Exceptions are the enemy in the engine, this is why you may find in other projects nothing seems to return a T& only T* - this is because you can test a T* for being a nullptr. Like with exceptions though, no one seems to bother.

Anyway, you should ask "how can I make debugging these easier?" the secret is the pre-processor.

Somewhere you need a config file and in it you shall write:

#define MYPROJ_SANITY_CHECK            1
#define MYPROJ_SANITY_CHECK_ASSETS     MYPROJ_SANITY_CHECK

and another file where you define this:

#define MYPROJ_EXIT(MSG) ::MyProject::exit(MSG,__LINE__,__FILE__);
namespace MyProject {
    exit(const char* message, int line, const char* file);
}

and make sure the implementation of exit outputs something on stderr and EXITS, not exceptions, insta-crash it! (Great place to set breakpoints!)

Then when you use an Asset (this is the hard part) you turn this:

use(incomingAsset);

into

#if MYPROJ_SANITY_CHECK_ASSETS == 1
    if(!incomingAsset->isLoaded()) {
        MYPROJ_EXIT("Not-loaded asset passed, loaded expected")
    }
#endif
use(incomingAsset)

This way you still get to do checks (which you want!) but you can turn them all off easily (which you also want!)


Real example

template<class T> class DeleteWhenDone;
namespace std {
template<class T>
void swap(DeleteWhenDone<T>&,DeleteWhenDone<T>&) noexcept;
}

template<class T>
class DeleteWhenDone {
public:
    DeleteWhenDone() { data = nullptr; }
    DeleteWhenDone(T* source): data(source) { }
    DeleteWhenDone(const DeleteWhenDone&) = delete;
    DeleteWhenDone(DeleteWhenDone&& from) noexcept: data(from.data) { from.data = nullptr; }
    DeleteWhenDone& operator=(const DeleteWhenDone&) = delete;
    DeleteWhenDone& operator=(DeleteWhenDone&& from) {
        DeleteWhenDone tmp(::std::forward<DeleteWhenDone>(from));
        ::std::swap(*this,tmp);
        return *this;
    }
    ~DeleteWhenDone() { delete data; }
private:
    T* data;

friend void ::std::swap<>(DeleteWhenDone&,DeleteWhenDone&) noexcept;
};
namespace std {

template<class T>
void swap(::DeleteWhenDone<T>& A, ::DeleteWhenDone<T>& B) noexcept {
    T* tmp = A.data;
    A.data = B.data;
    B.data = tmp;
}

}

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

Notice the default constructor and the way I implement the move operators, this is very common in systems programming and the compiler will optimise it.

I use this example to illustrate something unexpected. Notice I use nullptr as my "not initialised" value. Compiling with -O3 -fno-exceptions with the following test program

DeleteWhenDone<int> getInt() {
    DeleteWhenDone<int> a(new int(5));
    return a;
}
int main() {
    DeleteWhenDone<int> test;
    test = getInt();
}

we get the following code

0000000000400670 <main>:
  400670:       53                      push   %rbx
  400671:       bf 04 00 00 00          mov    $0x4,%edi
  400676:       e8 e5 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
  40067b:       31 ff                   xor    %edi,%edi
  40067d:       c7 00 05 00 00 00       movl   $0x5,(%rax)
  400683:       48 89 c3                mov    %rax,%rbx
  400686:       e8 85 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40068b:       31 ff                   xor    %edi,%edi
  40068d:       e8 7e ff ff ff          callq  400610 <operator delete(void*)@plt>
  400692:       48 89 df                mov    %rbx,%rdi
  400695:       e8 76 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40069a:       31 c0                   xor    %eax,%eax
  40069c:       5b                      pop    %rbx
  40069d:       c3                      retq   
  40069e:       66 90                   xchg   %ax,%ax

3 calls to delete!! This is BAD (although to a systems programmer this is great)

Earlier when I put the "checking for loadedness in a destructor" I meant it. If we change the destructor to:

    ~DeleteWhenDone() { if(data != nullptr) { delete data; } }

We get the following:

    0000000000400670 <main>:
      400670:       48 83 ec 08             sub    $0x8,%rsp
      400674:       bf 04 00 00 00          mov    $0x4,%edi
      400679:       e8 e2 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
      40067e:       c7 00 05 00 00 00       movl   $0x5,(%rax)
      400684:       48 89 c7                mov    %rax,%rdi
      400687:       e8 84 ff ff ff          callq  400610 <operator delete(void*)@plt>
      40068c:       31 c0                   xor    %eax,%eax
      40068e:       48 83 c4 08             add    $0x8,%rsp
      400692:       c3                      retq   
      400693:       66 66 66 66 2e 0f 1f    data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
      40069a:       84 00 00 00 00 00 

The last 2 lines are some sort of boundary padding.

The sub at the start moves the stack pointer back 8 bytes (a pointer size) so that the call to operator new returns the result exactly where it'll be needed (on main's stack). At the end it adds it again so when main returns the stack is as expected. You'll see how "truly optimum" this is.

This is the kind of stuff you need to be mindful of.

6 Rollback to Revision 4
source | link

So the other answer is good, but I will say this. In C++ the RAII idiom means "you never type object.close() or object.free() or object.release() or anything like that, it is a bit laxer on initialisation because of assignment operators.

Suppose we have the following

class Object {
public:
    Object() { /* CANNOT doStuff after this, as it isn't ready*/ }
    Object& operator=(const Object&);
    ~Object() { /*code to tidy up*/ }

    void load(Whatever);
    void doStuff();

private:
    /*implementation*/
};

I can now do stuff like:

Object o1; //not valid here
Object o2; //careful - you cannot use virtuals in a constructor!
o2.load(stuff); //o2 is now valid
o1=o2; //copy-assign, both valid

When this code-block ends both will automatically be destructed and clean up.

The important part is that the destructor clears up. This means if you have the following:

Asset::~Asset() {
    if(isLoaded()) {
        unload();
    }
}

Now your destructor will unload stuff - if it is loaded.

What you need is a way to communicate "you cannot use an asset that hasn't been loaded" without exceptions (as exceptions degrade performance quite severely)

If you use this then structures filled with assets will automatically unload when you destroy them, as it should be!


Exemplary uses of RAII

One of my all time favourite C++ things is:

1) code.whatever();
2) {Guard g(resource);
3)     do.this.that(BOTH).with(resource);
4) }
5) more.soForth();

g will be destroyed "at line 4" (proper scoping) thus automatically releasing the lock on resource it holds.


Game engines don't count

Note also that with game engines we care about performance NOT nice code. We're not systems programmers. A good example of this is when you have a virtual function you might need to call loads of times. It is far faster to have some sort of "type ID" and force a cast to the correct type then jump (without devirtualising) to something than it is to do the virtual call.

This is a total anti-pattern and if I saw a systems programmer doing it I'd probably break their legs. It's okay for us chosen few!


Why you need "invalid" things

Vectors and arrays are cheep. Even if you're going for maximum performance that default constructor running isn't going to be the bottleneck (letting the compiler see it in the header file will help as then it can inline it and wont have to do a full-on function call) and premature optimisation is evil.

Suppose you have a MyVector<T> which is actually a T[] underneath it all, which you get by doing data=new T[size] - this isn't bad to start with (even though it involves size calls to T::T()), the biggest problem here will be memory allocation (new becomes the enemy) - there is literally no need to optimise this now.

Now when you construct your T[] you need to have a default constructor (linked lists can get away without this as they construct new nodes as needed) and this is "classic C++" in the sense of "I know that the array is initialised" and I encourage you to do this even though it is "not as fast". If this is the bottleneck you can of course use malloc and inplace new everything you need. You just have to be very careful, so worry about this later.

NOTE ABOUT PITFALLS
If you change your MyVector implementation from something like:

  • data[size+1] = incoming; //will use T::operator= To

  • new(data+size+1) (incoming) //will use T::T

Which may require some tweaking if one of your T has an operator= for an obscure class but no constructor. Otherwise nothing will change.

OTHER PITFALL
This one is often missed, right now you have this magic global new - if that goes you'll require an Allocator& to everything that might have to allocate. Keep this in mind. If you do switch to a faster more game-friendly allocator, how will your objects get hold of a reference for it?


Life without exceptions

In theory stuff like thing.use(invalidAsset) should never happen, and usually when it does you'd have an exception.

Exceptions are the enemy in the engine, this is why you may find in other projects nothing seems to return a T& only T* - this is because you can test a T* for being a nullptr. Like with exceptions though, no one seems to bother.

Anyway, you should ask "how can I make debugging these easier?" the secret is the pre-processor.

Somewhere you need a config file and in it you shall write:

#define MYPROJ_SANITY_CHECK            1
#define MYPROJ_SANITY_CHECK_ASSETS     MYPROJ_SANITY_CHECK

and another file where you define this:

#define MYPROJ_EXIT(MSG) ::MyProject::exit(MSG,__LINE__,__FILE__);
namespace MyProject {
    exit(const char* message, int line, const char* file);
}

and make sure the implementation of exit outputs somethingPrivate messages on stderr and EXITS, not exceptions, insta-crash it! (Great place to set breakpoints!)

Then when you use an Asset (this is the hard part) you turn this:

use(incomingAsset);

into

#if MYPROJ_SANITY_CHECK_ASSETS == 1
    if(!incomingAsset->isLoaded()) {
        MYPROJ_EXIT("Not-loaded asset passed, loaded expected")
    }
#endif
use(incomingAsset)

This way you still get to site do checks (which you want!) but you can turn them all off easily (which you also want!)


Real example

template<class T> class DeleteWhenDone;
namespace std {
template<class T>
void swap(DeleteWhenDone<T>&,DeleteWhenDone<T>&) noexcept;
}

template<class T>
class DeleteWhenDone {
public:
    DeleteWhenDone() { data = nullptr; }
    DeleteWhenDone(T* source): data(source) { }
    DeleteWhenDone(const DeleteWhenDone&) = delete;
    DeleteWhenDone(DeleteWhenDone&& from) noexcept: data(from.data) { from.data = nullptr; }
    DeleteWhenDone& operator=(const DeleteWhenDone&) = delete;
    DeleteWhenDone& operator=(DeleteWhenDone&& from) {
        DeleteWhenDone tmp(::std::forward<DeleteWhenDone>(from));
        ::std::swap(*this,tmp);
        return *this;
    }
    ~DeleteWhenDone() { delete data; }
private:
    T* data;

friend void ::std::swap<>(DeleteWhenDone&,DeleteWhenDone&) noexcept;
};
namespace std {

template<class T>
void swap(::DeleteWhenDone<T>& A, ::DeleteWhenDone<T>& B) noexcept {
    T* tmp = A.data;
    A.data = B.data;
    B.data = tmp;
}

}

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

Notice the default constructor and the way I implement the move operators, this is very common in systems programming and the compiler will optimise it.

I use this example to illustrate something unexpected. Notice I use nullptr as my "not initialised" value. Compiling with -O3 -fno-exceptions with the following test program

DeleteWhenDone<int> getInt() {
    DeleteWhenDone<int> a(new int(5));
    return a;
}
int main() {
    DeleteWhenDone<int> test;
    test = getInt();
}

we get the following code

0000000000400670 <main>:
  400670:       53                      push   %rbx
  400671:       bf 04 00 00 00          mov    $0x4,%edi
  400676:       e8 e5 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
  40067b:       31 ff                   xor    %edi,%edi
  40067d:       c7 00 05 00 00 00       movl   $0x5,(%rax)
  400683:       48 89 c3                mov    %rax,%rbx
  400686:       e8 85 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40068b:       31 ff                   xor    %edi,%edi
  40068d:       e8 7e ff ff ff          callq  400610 <operator delete(void*)@plt>
  400692:       48 89 df                mov    %rbx,%rdi
  400695:       e8 76 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40069a:       31 c0                   xor    %eax,%eax
  40069c:       5b                      pop    %rbx
  40069d:       c3                      retq   
  40069e:       66 90                   xchg   %ax,%ax

3 calls to delete!! This is BAD (although to a systems programmer this is great)

Earlier when I put the "checking for loadedness in a destructor" I meant it. If we change the destructor to:

    ~DeleteWhenDone() { if(data != nullptr) { delete data; } }

We get the following:

    0000000000400670 <main>:
      400670:       48 83 ec 08             sub    $0x8,%rsp
      400674:       bf 04 00 00 00          mov    $0x4,%edi
      400679:       e8 e2 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
      40067e:       c7 00 05 00 00 00       movl   $0x5,(%rax)
      400684:       48 89 c7                mov    %rax,%rdi
      400687:       e8 84 ff ff ff          callq  400610 <operator delete(void*)@plt>
      40068c:       31 c0                   xor    %eax,%eax
      40068e:       48 83 c4 08             add    $0x8,%rsp
      400692:       c3                      retq   
      400693:       66 66 66 66 2e 0f 1f    data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
      40069a:       84 00 00 00 00 00 

The last 2 lines are some sort of boundary padding.

The sub at the start moves the stack pointer back 8 bytes (a pointer size) so that the call to operator new returns the result exactly where it'll be needed (on main's stack). At the end it adds it again so when main returns the stack is as expected. You'll see how "truly optimum" this is.

This is the kind of stuff you need to be mindful of.exist BTW

So the other answer is good, but I will say this. In C++ the RAII idiom means "you never type object.close() or object.free() or object.release() or anything like that, it is a bit laxer on initialisation because of assignment operators.

Suppose we have the following

class Object {
public:
    Object() { /* CANNOT doStuff after this, as it isn't ready*/ }
    Object& operator=(const Object&);
    ~Object() { /*code to tidy up*/ }

    void load(Whatever);
    void doStuff();

private:
    /*implementation*/
};

I can now do stuff like:

Object o1; //not valid here
Object o2; //careful - you cannot use virtuals in a constructor!
o2.load(stuff); //o2 is now valid
o1=o2; //copy-assign, both valid

When this code-block ends both will automatically be destructed and clean up.

The important part is that the destructor clears up. This means if you have the following:

Asset::~Asset() {
    if(isLoaded()) {
        unload();
    }
}

Now your destructor will unload stuff - if it is loaded.

What you need is a way to communicate "you cannot use an asset that hasn't been loaded" without exceptions (as exceptions degrade performance quite severely)

If you use this then structures filled with assets will automatically unload when you destroy them, as it should be!


Exemplary uses of RAII

One of my all time favourite C++ things is:

1) code.whatever();
2) {Guard g(resource);
3)     do.this.that(BOTH).with(resource);
4) }
5) more.soForth();

g will be destroyed "at line 4" (proper scoping) thus automatically releasing the lock on resource it holds.


Game engines don't count

Note also that with game engines we care about performance NOT nice code. We're not systems programmers. A good example of this is when you have a virtual function you might need to call loads of times. It is far faster to have some sort of "type ID" and force a cast to the correct type then jump (without devirtualising) to something than it is to do the virtual call.

This is a total anti-pattern and if I saw a systems programmer doing it I'd probably break their legs. It's okay for us chosen few!


Why you need "invalid" things

Vectors and arrays are cheep. Even if you're going for maximum performance that default constructor running isn't going to be the bottleneck (letting the compiler see it in the header file will help as then it can inline it and wont have to do a full-on function call) and premature optimisation is evil.

Suppose you have a MyVector<T> which is actually a T[] underneath it all, which you get by doing data=new T[size] - this isn't bad to start with (even though it involves size calls to T::T()), the biggest problem here will be memory allocation (new becomes the enemy) - there is literally no need to optimise this now.

Now when you construct your T[] you need to have a default constructor (linked lists can get away without this as they construct new nodes as needed) and this is "classic C++" in the sense of "I know that the array is initialised" and I encourage you to do this even though it is "not as fast". If this is the bottleneck you can of course use malloc and inplace new everything you need. You just have to be very careful, so worry about this later.

NOTE ABOUT PITFALLS
If you change your MyVector implementation from something like:

  • data[size+1] = incoming; //will use T::operator= To

  • new(data+size+1) (incoming) //will use T::T

Which may require some tweaking if one of your T has an operator= for an obscure class but no constructor. Otherwise nothing will change.

OTHER PITFALL
This one is often missed, right now you have this magic global new - if that goes you'll require an Allocator& to everything that might have to allocate. Keep this in mind. If you do switch to a faster more game-friendly allocator, how will your objects get hold of a reference for it?


Life without exceptions

In theory stuff like thing.use(invalidAsset) should never happen, and usually when it does you'd have an exception.

Exceptions are the enemy in the engine, this is why you may find in other projects nothing seems to return a T& only T* - this is because you can test a T* for being a nullptr. Like with exceptions though, no one seems to bother.

Anyway, you should ask "how can I make debugging these easier?" the secret is the pre-processor.

Somewhere you need a config file and in it you shall write:

#define MYPROJ_SANITY_CHECK            1
#define MYPROJ_SANITY_CHECK_ASSETS     MYPROJ_SANITY_CHECK

and another file where you define this:

#define MYPROJ_EXIT(MSG) ::MyProject::exit(MSG,__LINE__,__FILE__);
namespace MyProject {
    exit(const char* message, int line, const char* file);
}

and make sure the implementation of exit outputs something on stderr and EXITS, not exceptions, insta-crash it! (Great place to set breakpoints!)

Then when you use an Asset (this is the hard part) you turn this:

use(incomingAsset);

into

#if MYPROJ_SANITY_CHECK_ASSETS == 1
    if(!incomingAsset->isLoaded()) {
        MYPROJ_EXIT("Not-loaded asset passed, loaded expected")
    }
#endif
use(incomingAsset)

This way you still get to do checks (which you want!) but you can turn them all off easily (which you also want!)


Real example

template<class T> class DeleteWhenDone;
namespace std {
template<class T>
void swap(DeleteWhenDone<T>&,DeleteWhenDone<T>&) noexcept;
}

template<class T>
class DeleteWhenDone {
public:
    DeleteWhenDone() { data = nullptr; }
    DeleteWhenDone(T* source): data(source) { }
    DeleteWhenDone(const DeleteWhenDone&) = delete;
    DeleteWhenDone(DeleteWhenDone&& from) noexcept: data(from.data) { from.data = nullptr; }
    DeleteWhenDone& operator=(const DeleteWhenDone&) = delete;
    DeleteWhenDone& operator=(DeleteWhenDone&& from) {
        DeleteWhenDone tmp(::std::forward<DeleteWhenDone>(from));
        ::std::swap(*this,tmp);
        return *this;
    }
    ~DeleteWhenDone() { delete data; }
private:
    T* data;

friend void ::std::swap<>(DeleteWhenDone&,DeleteWhenDone&) noexcept;
};
namespace std {

template<class T>
void swap(::DeleteWhenDone<T>& A, ::DeleteWhenDone<T>& B) noexcept {
    T* tmp = A.data;
    A.data = B.data;
    B.data = tmp;
}

}

The STL isn't enough for engines. http://stackoverflow.com/a/20290701/2112028 (check out this answer on a fast shared pointer, the STL is a systems programming perspective, it's good, but not for engines) So we write our own. This (missing an operator->) class is an example of something that takes whatever it points to with it when it is destroyed. Say you have a vector of these, or an array - when it is destroyed these pointers delete what they point to with it.

Notice the default constructor and the way I implement the move operators, this is very common in systems programming and the compiler will optimise it.

I use this example to illustrate something unexpected. Notice I use nullptr as my "not initialised" value. Compiling with -O3 -fno-exceptions with the following test program

DeleteWhenDone<int> getInt() {
    DeleteWhenDone<int> a(new int(5));
    return a;
}
int main() {
    DeleteWhenDone<int> test;
    test = getInt();
}

we get the following code

0000000000400670 <main>:
  400670:       53                      push   %rbx
  400671:       bf 04 00 00 00          mov    $0x4,%edi
  400676:       e8 e5 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
  40067b:       31 ff                   xor    %edi,%edi
  40067d:       c7 00 05 00 00 00       movl   $0x5,(%rax)
  400683:       48 89 c3                mov    %rax,%rbx
  400686:       e8 85 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40068b:       31 ff                   xor    %edi,%edi
  40068d:       e8 7e ff ff ff          callq  400610 <operator delete(void*)@plt>
  400692:       48 89 df                mov    %rbx,%rdi
  400695:       e8 76 ff ff ff          callq  400610 <operator delete(void*)@plt>
  40069a:       31 c0                   xor    %eax,%eax
  40069c:       5b                      pop    %rbx
  40069d:       c3                      retq   
  40069e:       66 90                   xchg   %ax,%ax

3 calls to delete!! This is BAD (although to a systems programmer this is great)

Earlier when I put the "checking for loadedness in a destructor" I meant it. If we change the destructor to:

    ~DeleteWhenDone() { if(data != nullptr) { delete data; } }

We get the following:

    0000000000400670 <main>:
      400670:       48 83 ec 08             sub    $0x8,%rsp
      400674:       bf 04 00 00 00          mov    $0x4,%edi
      400679:       e8 e2 ff ff ff          callq  400660 <operator new(unsigned long)@plt>
      40067e:       c7 00 05 00 00 00       movl   $0x5,(%rax)
      400684:       48 89 c7                mov    %rax,%rdi
      400687:       e8 84 ff ff ff          callq  400610 <operator delete(void*)@plt>
      40068c:       31 c0                   xor    %eax,%eax
      40068e:       48 83 c4 08             add    $0x8,%rsp
      400692:       c3                      retq   
      400693:       66 66 66 66 2e 0f 1f    data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
      40069a:       84 00 00 00 00 00 

The last 2 lines are some sort of boundary padding.

The sub at the start moves the stack pointer back 8 bytes (a pointer size) so that the call to operator new returns the result exactly where it'll be needed (on main's stack). At the end it adds it again so when main returns the stack is as expected. You'll see how "truly optimum" this is.

This is the kind of stuff you need to be mindful of.

Private messages on this site do exist BTW

5 Rollback to Revision 3
source | link
4 deleted 10215 characters in body
source | link
3 added 4549 characters in body
source | link
2 added 3362 characters in body
source | link
1
source | link