Rule of Three, and const-correctness questions

Started by
38 comments, last by SiCrane 10 years, 8 months ago

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 )?

Advertisement

The reason for the C++ rule of three is that it's highly possible that if you had to implement your own copy-constructor, destructor or copy assignment operator because you needed different behaviour from the compiler-generated versions, the other 2 functions won't suit your needs either.

An example


class Foo
{

public:
  
  Foo()
  {
    m_bar = new Bar();
  }

  ~Foo()
  {
    delete m_bar;
  }

private:

  Bar* m_bar;
  
};

In this example the default compiler-generated copy constructor or copy assignment operator would just copy the given pointer over to the destination instance. This would cause undefined behaviour once the destructor of a copy instance of Foo would get called.

The issue of const-correctness goes much deeper than just function arguments, it also applies to class methods (does a certain method change class data, or not?), return values, etc.

The easiest way to think of it is that if your data should remain immutable you should declare it as const. If a method operating on class data does not change this data, it should be declared const as well.


class Foo
{
public:

  // We declare this method as const, since it doesn't modify any data
  int getSomething() const { return m_something; }  

  // This method modifies our variable, so it shouldn't be declared as const
  void modifySomething(int n) { m_something += n; }

private:

  int m_something;   
};

To address your example about your Update function, passing a const argument by value is not what you're looking for. When you pass an argument by value, its contents will get pushed on the stack, so if you pass a variable as an argument to this function, the original variable will not get modified.

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:


class Foo
{
public:

  void doSomething(const Bar& b);
  void doSomething(Bar& b);
};

I gets all your texture budgets!

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.

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.

-Lead developer for OutpostHD

http://www.lairworks.com

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.

I know what the Rule Of Three is for (creating copy ctor, assignment ctor, and dtor), but I don't completely understand why.

The rule is that if you have 1 of those things (a copy ctor, assignment op, or dtor), then you very probably need to have all 3 of them in order to avoid bugs.

Radikalizm's first example is a good one -- it relies on the destructor to clean up resources for it... however, that's 1 of the 3 things, with the other 2 missing. If someone copies/assigns that class, then the automatically-generated copy-ctor/assignment-op aren't going to do the right thing (which is to use new to create their own m_bar object).

There's a wikipedia page on the rule here: http://en.wikipedia.org/wiki/Rule_of_three_(C++_programming)

Regarding const correctness, the C++ FAQ does a good job of explaining here: http://www.parashift.com/c++-faq/const-correctness.html

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;

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.

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.

throw table_exception("(? ???)? ? ???");

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
}

This topic is closed to new replies.

Advertisement