Jump to content
  • Advertisement
Sign in to follow this  
johnmarinelli

Rule of Three, and const-correctness questions

This topic is 1905 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

Hi guys,

 

I was recently told that my code is missing the Rule Of Three concept, and that my timer isn't const-correct.

 

I know of these terms; but google and searching the forums didn't help much.

 

I think what would help, however, would be an example of each concept (Rule of Three, Const-correctness) related to Game Programming.  I know what the Rule Of Three is for (creating copy ctor, assignment ctor, and dtor), but I don't completely understand why.  Also, I know that RAII/smart pointers take care of all that altogether, but I'd like to understand the Rule of Three.

 

As far as const-correctness, I think that it means that when you pass a variable by reference to a function, you should pass it as a const so that you're assuring it doesn't get changed.  does this mean that in my game loop, the Update(int &deltaTicks) function should be Update( const int &deltaTicks )?

Edited by johnmarinelli

Share this post


Link to post
Share on other sites
Advertisement

Ive met a few programmers but I never heard of Rule of Three.

Using consts makes more sense. Yes, it should be Update( const int deltaTicks ), ....but no big deal if you dont use const, but its highly recommented if other people are working on that code too. And you dont have to pass an int by reference when you want it to be const.

Share this post


Link to post
Share on other sites

Ive met a few programmers but I never heard of Rule of Three.

Using consts makes more sense. Yes, it should be Update( const int deltaTicks ), ....but no big deal if you dont use const, but its highly recommented if other people are working on that code too. And you dont have to pass an int by reference when you want it to be const.

 

Using const in the Update() function like that is useless in this case. The int parameter is being passed by value, not by reference. By value doesn't need a const qualifier because the value is copied, not referenced. You only need a const qualifier if you're passing by reference/pointer and you don't want the function to change the object being passed.

 

So to answer the OP's last question directly, no. You're not passing by reference there, you're passing by value so the const qualifier is useless.

Edited by leeor_net

Share this post


Link to post
Share on other sites

Using const on an int as a parameter isn't useless - it's documenting. That said, it's not common as it is really only documenting. Using const on a return value that is passed by value is useless as the value can only be copied from, not assigned to, in any case.

Share this post


Link to post
Share on other sites

This does become an issue however when working with references. Try to find out for yourself what could happen to the arguments of type Bar in the following methods:

 

Well, it looks like in the first example, b won't get changed where in the second example it could get changed.  I guess I'm overthinking this const-correctness thing smile.png

However, I (think) I still don't understand the uses of the copy ctor/assignment op;

class Foo{
private:
    int data
public:
/*normal ctor*/
    Foo(int d) : data(d) {};
/*copy ctor*/
    Foo(const &Foo copy) : data(copy.data){};
/*assignment op*/
    Foo& operator=(const &Foo copy){ data = copy.data };
};

is the above correct? 

if so, what do they have to do with dynamically allocated memory?

Foo *_ptr = new Foo();

/*does this create a whole new space in memory for test?
  if it does, am I correct in saying that the default assign op
  only makes test point to the same area of memory as _ptr - hence the whole
  deal about copy assignment operators?*/
Foo test = *_ptr;
Edited by johnmarinelli

Share this post


Link to post
Share on other sites

Ive met a few programmers but I never heard of Rule of Three.

Using consts makes more sense. Yes, it should be Update( const int deltaTicks ), ....but no big deal if you dont use const, but its highly recommented if other people are working on that code too. And you dont have to pass an int by reference when you want it to be const.

 

Using const in the Update() function like that is useless in this case. The int parameter is being passed by value, not by reference. By value doesn't need a const qualifier because the value is copied, not referenced. You only need a const qualifier if you're passing by reference/pointer and you don't want the function to change the object being passed.

 

So to answer the OP's last question directly, no. You're not passing by reference there, you're passing by value so the const qualifier is useless.

 

"Using const in the Update() function like that is useless in this case"

...well, he said its a reference:), I thought he just forgot the &

 

"You only need a const qualifier if you're passing by reference/pointer and you don't want the function to change the object being passed."

...remove the word only from this sentence and it will be correct.

Share this post


Link to post
Share on other sites

You only need a const qualifier if you're passing by reference/pointer and you don't want the function to change the object being passed.


In addition to documenting your intent, const does have a function in this case -- it prevents you from modifying the argument inside the function body. I know it might seem useless to enforce rules on yourself, but this kind of defensive coding helps you to not make trivial mistakes, and prevent future code maintainers from violating your assumptions -- e.g. if your implementation assumes the argument to be constant, but you don't mark it as such, and someone comes along later to fix a bug and modifies it.

I would, and in fact do, go so far as to pass all arguments as const by default, regardless of whether they are by value or by reference. In most cases I would even go one further and take a local copy of that const parameter (in the smallest possible scope) if I wanted to modify it. One place I *don't* do this is when I mean to enable the RVO (the Return Value Optimization), as I do when implementing operator+ in terms of operator+= in my classes. Edited by Ravyne

Share this post


Link to post
Share on other sites

However, I (think) I still don't understand the uses of the copy ctor/assignment op;

class Foo{
private:
    int data
public:
/*normal ctor*/
    Foo(int d) : data(d) {};
/*copy ctor*/
    Foo(const &Foo copy) : data(copy.data){};
/*assignment op*/
    Foo& operator=(const &Foo copy){ data = copy.data };
};
is the above correct?

Well, it behaves as you'd expect... except the assignment operator should be:
Foo& operator=(const &Foo copy){ data = copy.data; return *this; };
It breaks the rule of 3 though -- either you should have 0 of the 3 things in the rule, or 3 of the 3 things. You've only written 2 of them. The rule is: if you have a copy-ctor, assignment-op or destructor, you should have all three of them.

However, the copy-ctor and assignment-op aren't needed for such a simple class -- the compiler will generate them if you don't write them yourself, so that class should just be:

class Foo{
    int data
public:
    Foo(int d) : data(d) {};
};

This also satifies the rule, because you have 0 out of 3 of those things now.

if so, what do they have to do with dynamically allocated memory?

In your example, the only member is an int, so no custom copy/assignment/destructor logic is required.
If your class manages resources (like dynamic memory), then it now needs all 3 of them:

class Foo{
private:
    int* data
public:
/*normal ctor*/
    Foo(int d) : data( new int(d) ) {}; //the default would not initialize data at all, we want to allocate some memory
/*copy ctor*/
    Foo(const &Foo copy) : data( new int(*copy.data) ){};
   // the default would be: Foo(const &Foo copy) : data(copy.data)
   // we instead want to allocate a new int, and copy the other's value into our new one
/*assignment op*/
    Foo& operator=(const &Foo copy) // the default would be: data = copy.data; return *this;
    {//we don't want copy the address/pointers, we need to copy the value located at one pointer to the variable pointed to by the other
        *data = *copy.data;
        return *this;
    }
/*destructor*/
    ~Foo() { delete data; } //the default would do nothing, we need to clean up our memory allocation
};

The above custom implementations ensure that every Foo owns it's own int (that is created with new) and also cleans up after itself (using delete in the destructor).
If you let the compiler implement those 3 functions for you, it would simply copy the pointer around the place, and multiple Foo objects would end up sharing the same int object.

Foo *_ptr = new Foo();
/*does this create a whole new space in memory for test?
  if it does, am I correct in saying that the default assign op
  only makes test point to the same area of memory as _ptr - hence the whole
  deal about copy assignment operators?*/
Foo test = *_ptr;

This is a whole different question, and shows you need to first learn about stack vs heap allocations (or automatic storage vs free-store storage in strict C++ terminology).

When you write: int foo = 42; the variable 'foo' is created in the call-stack. It's a "temporary variable" that only exists for the lifetime of the function. When the function returns, 'foo' disappears.

When you write int* foo = new int(42), the variable 'foo' is a temporary on the call-stack as before, but this variable just stores the address of a memory allocation elsewhere. It's your responsibility to ensure that memory allocation is cleaned up with delete.

You can use complex types instead of int and everything works the same, except that when you create the variable, the constructor is called, and when the function returns, the destructor is called.

void test1()
{
  Foo test;//memory allocated on the stack, constructor is called;
  return;//destructor is called, stack is unwound
}
void test2()
{
  Foo* test;//memory for 'test' (the address/pointer variable) on the stack.
  test = new Foo();//Memory for a Foo object created on the heap, constructor is called, address of allocation stored in test
  delete test;//destructor called, heap allocation is cleaned up
  return;//stack is unwound
}
void test3(Foo* input)//this argument contains the address of a Foo object
{
  Foo test;//default constructor called, object exists on the stack
  test = *input;//assignment operator is called, which should copy the values located at the input allocation into the stack allocation at &test
  return;//test's destructor is called
}
Edited by Hodgman

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.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!