Search

9.x — Chapter 9 comprehensive quiz

In this chapter, we explored topics related to operator overloading, as well as overloaded typecasts, and topics related to the copy constructor.

Summary

Operator overloading is a variant of function overloading that lets you overload operators for your classes. When operators are overloaded, the intent of the operators should be kept as close to the original intent of the operators as possible. If the meaning of an operator when applied to a custom class is not clear and intuitive, use a named function instead.

Operators can be overloaded as a normal function, a friend function, or a member function. The following rules of thumb can help you determine which form is best for a given situation:

  • If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.
  • If you’re overloading a unary operator, do so as a member function.
  • If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), do so as a member function if you can.
  • If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function or friend function.

Typecasts can be overloaded to provide conversion functions, which can be used to explicitly or implicitly convert your class into another type.

A copy constructor is a special type of constructor used to initialize an object from another object of the same type. Copy constructors are used for direct/uniform initialization from an object of the same type, copy initialization (Fraction f = Fraction(5,3)), and when passing or returning a parameter by value.

If you do not supply a copy constructor, the compiler will create one for you. Compiler-provided copy constructors will use memberwise initialization, meaning each member of the copy is initialized from the original member. The copy constructor may be elided for optimization purposes, even if it has side-effects, so do not rely on your copy constructor actually executing.

Constructors are considered converting constructors by default, meaning that the compiler will use them to implicitly convert objects of other types into objects of your class. You can avoid this by using the explicit keyword in front of your constructor. You can also delete functions within your class, including the copy constructor and overloaded assignment operator if desired. This will cause a compiler error if a deleted function would be called.

The assignment operator can be overloaded to allow assignment to your class. If you do not provide an overloaded assignment operator, the compiler will create one for you. Overloaded assignment operators should always include a self-assignment check.

New programmers often mix up when the assignment operator vs copy constructor are used, but it’s fairly straightforward:

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

By default, the copy constructor and assignment operators provided by the compiler do a memberwise initialization or assignment, which is a shallow copy. If your class dynamically allocates memory, this will likely lead to problems, as multiple objects will end up pointing to the same allocated memory. In this case, you’ll need to explicitly define these in order to do a deep copy. Even better, avoid doing your own memory management if you can and use classes from the standard library.

Quiz Time

1) Assuming Point is a class and point is an instance of that class, should you use a normal/friend or member function overload for the following operators?

1a) point + point
1b) -point
1c) std::cout << point
1d) point = 5;

Show Solution

2) Write a class named Average that will keep track of the average of all integers passed to it. Use two members: The first one should be type int32_t, and used to keep track of the sum of all the numbers you’ve seen so far. The second should be of type int8_t, and used to keep track of how many numbers you’ve seen so far. You can divide them to find your average.

2a) Write all of the functions necessary for the following program to run:

and produce the result:

4
6
12
6.5
7
7

Hint: Remember that int8_t is usually typedef’d as a char, so std::cout treats it accordingly.

Show Solution

2b) Does this class need an explicit copy constructor or assignment operator?

Show Solution

3) Write your own integer array class named IntArray from scratch (do not use std::array or std::vector). Users should pass in the size of the array when it is created, and the array should be dynamically allocated. Use assert statements to guard against bad data. Create any constructors or overloaded operators needed to make the following program operate correctly:

This programs should print:

5 8 2 3 6
5 8 2 3 6

Show Solution

4) Extra credit: This one is a little more tricky. A floating point number is a number with a decimal where the number of digits after the decimal can be variable. A fixed point number is a number with a fractional component where the number of digits in the fractional portion is fixed.

In this quiz, we’re going to write a class to implement a fixed point number with two fractional digits (e.g. 12.34, 3.00, or 1278.99). Assume that the range of the class should be -32768.99 to 32767.99, that the fractional component should hold any two digits, that we don’t want precision errors, and that we want to conserve space.

4a) What type of member variable(s) do you think we should use to implement our fixed point number with 2 digits after the decimal? (Make sure you read the answer before proceeding with the next questions)

Show Solution

4b) Write a class named FixedPoint2 that implements the recommended solution from the previous question. If either (or both) of the non-fractional and fractional part of the number are negative, the number should be treated as negative. Provide the overloaded operators and constructors required for the following program to run:

This program should produce the result:

34.56
-2.08
-2.08
-2.08
-0.05
-0.05

Hint: Although it may initially seem like more work initially, it’s helpful to store both the non-fractional and fractional parts of the number with the same sign (e.g. both positive if the number is positive, and both negative if the number is negative). This makes doing math much easier later.
Hint: To output your number, first cast it to a double.

Show Solution

4c) Now add a constructor that takes a double. You can round a number (on the left of the decimal) by using the round() function (included in header cmath).

Hint: You can get the non-fractional part of a double by static casting the double to an integer
Hint: To get the fractional part of a double, you’ll first need to zero-out the non-fractional part. Use the integer value to do this.
Hint: You can move a digit from the right of the decimal to the left of the decimal by multiplying by 10. You can move it two digits by multiplying by 100.

The follow program should run:

This program should produce the result

0.01
-0.01
5.01
-5.01

Show Solution

4d) Overload operator==, operator >>, operator- (unary), and operator+ (binary).

The following program should run:

And produce the output:

true
true
true
true
true
true
true
true
-0.48
0.48
Enter a number: 5.678
You entered: 5.68

Hint: Add your two FixedPoint2 together by leveraging the double cast, adding the results, and converting back to a FixedPoint2.
Hint: For operator>>, use your double constructor to create an anonymous object of type FixedPoint2, and assign it to your FixedPoint2 function parameter

Show Solution

10.1 -- Object relationships
Index
9.15 -- Shallow vs. deep copying

258 comments to 9.x — Chapter 9 comprehensive quiz

  • Anastasia

    Hi!
    Here's my 3 (with some wannabe std::vector<int> functionality :D ).

    @nascardriver, only code up to line 120 in `IntArray.cpp` (up to `empty()` function) is related to the quiz, so if you don't have time/ don't want to look at the rest - just ignore it. Although it would certainly help me a lot to see your suggestions and corrections about everything, any feedback is greatly appreciated!

    And no pointer arithmetics there anymore :)

    IntArray.h

    IntArray.cpp

    alexMain.cpp

    I don't like my `fill()` function (lines 102-115) - neither using it in the constructor, nor its implementation (although I tried to make it as useful as possible). But since Alex was using `[]` operator on the memory which would be inaccessible to this operator in my implementation (because I don't initialize it when allocating (for several reasons)) I had to add it :/ Any thoughts on what could I've possibly done instead are welcome.
    Also, Valgrind is complaining about 4 bytes (size of an int on my system) being written by `fill()` to an address it shouldn't access to (valgrind thinks so, anyway). 2 times actually (because the default constructor which calls this function was invoked 2 times). I don't know whether it's something I should be worried about.

    Not quiz related:
    I know that my operator overloadings may not have much logical sense, but because this is what the chapter was about, this was what I wanted to practice. I've made all the overloadings member functions, because all of them are modifying the left operand(this). I'd like to know whether it was the right approach (I don't mean here the operators required by the quiz -- with those I didn't have much choice). And of course I'd very appreciate any suggestions on how to make the implementations more efficient as well.

    edit: fixed an error -- lines 112-113 should be

    not:

    • `m_places_left`. Interesting, I've only every seen "capacity". You need to update `m_places_left` whenever the array changes, capacity would stay the same.
      You already noticed that your operators aren't quite self-explanatory. I suggest you to get into the habit of documenting your code using documentation comments. They help the user of your code (Yourself, if the project is big enough) and can provide rich autocomplete if your editor supports it.

      - If you share your code as a library, the user will only see the header. If there are no parameter names in the header, your library is very hard to use.
      - Use `std::size_t`, `size_t` is C.
      - In the `std` library, whenever you supply an end index, that index is exclusive. Your `fill` uses an inclusive end index (Should be mentioned in a doc-comment). That's fine, but might cause trouble (At least thinking time) when your class is used alongside something from the `std` library.
      - When you supply operators to do something, also add a named function. Otherwise you have to do things like in line 113 when you have a pointer.
      - Line 131, 138: That's undefined behavior if `m_places_left` is 0. This is a case where pointer arithmetic should be used.

      - If you're using the initial value of a variable, explicitly initialize it (Line 182).
      - Line 191: Should be declared in the loop's header.
      - Line 204, 205: The duplicate calculation can be removed by calculating the missing capacity first and comparing it to 0.
      - Line 207, 208: You know that you won't run out of memory and you can calculate the new sizes. `operator<<` is wasteful in this scenario.

      > neither using it [fill] in the constructor
      > since Alex was using `[]` operator on the memory which would be inaccessible to this operator
      But he writes to it, that's ok. If you add a doc-comment stating that the memory is uninitialized, there's no problem.
      The constructor doesn't need to use `getMemory`. All you need is line 71 (modified to default-initialize into `m_array`).

      > nor its [fill's] implementation
      - Line 107-113 should be done before the loop starts.

      > Valgrind is complaining
      `fill`'s `end` is inclusive.
      Please, whenever you get an error or something, post the full error message with line numbers.

      > I've made all the overloadings member functions [...]. I'd like to know whether it was the right approach
      Yes. None of them could have a lhs operand that's not an instance of your class. Unless you think `5 + arr` makes sense. I'd say your `operator+` should be commutative.

      > more efficient
      You're overusing `freeMemory`. None of the places you're using `freeMemory` in actually needs to do everything that `freeMemory` does.
      If you apply my comments, I don't see anything more as of now.

      • Anastasia

        > `m_places_left`. Interesting
        It was the first thing that came to my mind as a control tool of the allocated memory. It probably seems as a naive approach (not only this, but I'd guess all of my implementations), because it was my first attempt of writing something of the sort.

        > If there are no parameter names in the header, your library is very hard to use.
        Can I provide parameter names just for some of the functions, those that are not self-explanatory (like `fill()` or op.overloadings), not for all?

        > Use `std::size_t`, `size_t` is C.
        Fixed. I just casted to the most reasonable type (which accidentally happened to exist) that came to my mind in order to get rid of the warning. It's awful programming, I know :D

        > When you supply operators to do something, also add a named function
        I'm sorry that it wasn't very clear, can I just add a commentary next time (instead of duplicating code with named functions)?

        > Line 131, 138: That's undefined behavior if `m_places_left` is 0. This is a case where pointer arithmetic should be used.
        Thank you, I'd have never thought it's UB! Maybe it's safer (for now) to use pointers everywhere again? Because I just can't see the situations like that...

        > If you're using the initial value of a variable, explicitly initialize it (Line 182).
        - Line 191: Should be declared in the loop's header.
        line 182 -- fair enough, line 191 -- it was intentional, otherwise the loop's header would be too long. I could use the same variable (index_to_erase) in the second loop as well, but I wanted to differentiate between those two things (finding the right index and shifting all the others), for clarity. I could also make the name of `index_to_erase` shorter, but again, I wanted it be self-explanatory, so I decided to move it outside of the header - it seemed readable and clear to me.

        >  Line 204, 205: The duplicate calculation can be removed by calculating the missing capacity first and comparing it to 0.
        - Line 207, 208: You know that you won't run out of memory and you can calculate the new sizes. `operator<<` is wasteful in this scenario.

        I simplified it to this:

        And same for the `operator-=`. You're right `<<` can take care of memory anyway.

        > The constructor doesn't need to use `getMemory`. All you need is line 71 (modified to default-initialize into `m_array`).
        I see your point, but I don't like the idea of the memory requested by the user being initialized right away -- they are supposed to push the values (or copy them) first, before accessing them. This way (intializing it with 0s) I'm forcing the user to use `[]` (like Alex did) to overwrite the values (this memory it meant to be overwritten, why bother filling it -- I did it, but just for the quiz).

        > Line 107-113 should be done before the loop starts.
        Thank you, I think I also should reserve memory (if needed) before running the loop, instead of constantly checking it :/

        > Valgrind is complaining -- post the full error message with line numbers
        Oh, there's no line numbers - just memory addresses. It's not very clear and quite long. But in case it would help to identify the problem (2 identical errors like the one below):

        > [right approach] Yes. None of them could have a lhs operand that's not an instance of your class. Unless you think `5 + arr` makes sense. I'd say your `operator+` should be commutative.
        I'm glad something I did right. And I don't think `5 + arr` makes sense... If by saying that it should you are intending that my overloading of the `operator+` was silly - I totally agree :D

        > You're overusing `freeMemory`
        I may need in the future... It's more `reset` than `freeMemory` though :/

        Thank you so much for all the suggestions, I'll try to apply everything (to this and my future codes as well).
        And you are right - I need to learn to properly comment my code, thanks for your advice about that as well!

        • Anastasia

          Please, ignore the error output. Valgrind is happy after I moved the assignment line (`m_array[element] = value;`) in `fill()` function under the things making sure that m_length and memory are in order (as you said). Thank you!

        • > Can I provide parameter names just for some of the functions, those that are not self-explanatory
          Apart from the copy constructor and copy assignment operator, I don't think any parameters are self-explanatory.

          > When you supply operators to do something, also add a named function\

          Neither of these is particularly attractive. It'd be nicer to have a named function.

          The operators then just forward to call to the named function.

          > Maybe it's safer (for now) to use pointers everywhere again?
          If you want to have the address of an element, you can use pointer arithmetic. If you want to have the element itself, use array syntax.

          > I just can't see the situations like that
          You're accessing an element that doesn't exist. If you know the size of your array, this is relatively easy to spot. You have to learn it at some point, so you might as well do it right from the beginning.

          > the loop's header would be too long
          You seem to be targeting a very short line length. Sooner or later you'll have to start splitting lines

          You shouldn't let a naming or formatting policy decrease your code's quality.

          > You're right `<<` can take care of memory anyway.
          Nooo! That's worse than before.
          If `that` has 10000 elements, you'll be calling `outOfMemory` 10000 times and and reallocate up to 1000 times. In the previous version you at least didn't have the reallocations.

          > I don't like the idea of the memory requested by the user being initialized right away
          If it's initialization vs no initialization and assignment, initialization wins.

          > This way (intializing it with 0s) I'm forcing the user to use `[]` (like Alex did) to overwrite the values
          I don't see how that's related or where you're forcing the user to do anything.
          If your goal is to get close to `std::vector`, the fill constructor should default-initialize the elements and they should be accessible.

          > Oh, there's no line numbers
          Are you building in debug mode?

          valgrind-3.15.0

          > I moved the assignment line [...]
          Your fill is wrong and/or you're using it wrong. Rubber duck your code with an array of length 1.

          • Anastasia

            > Apart from the copy constructor and copy assignment operator, I don't think any parameters are self-explanatory.
            I my case yes, they aren't... But I wanted to know whether it would be considered weird (or bad practice) if I provide parameter names just for some functions, but not for all of them.

            > You shouldn't let a naming or formatting policy decrease your code's quality.
            I think it's an experience thing - at the moment it's hard to spot things that are undesirable and negatively impacting the code quality. Is it okay to split for-loop's header like that?

            > Nooo! That's worse than before.
            Oops, sorry :( I like your version a lot - it's clear what it does and why and it doesn't duplicate code (using `copyElements()` didn't even come to my mind).

            > I don't see how that's related or where you're forcing the user to do anything.
            I probably just wanted to force them to use `<<` or `+=` :D

            > Are you building in debug mode?
            Nope, I didn't set the flag while compiling...Sorry for that. I'm neglecting and ignoring debugging for the most part, using only `cerr`s and occasionally basic valgrind memcheck when it's about allocated memory :/ Need to re-read chapter 3 about debugging - but I believe it's all about IDEs...

            > Your fill is wrong
            I think I got it, thanks. I didn't like what I did there from the very beginning.

            The rest is clear - thank you for all the replies!

            • > I wanted to know whether it would be considered weird (or bad practice) if I provide parameter names just for some functions, but not for all of them.
              I'd say yes.

              > Is it okay to split for-loop's header like that?
              No. `for`-loops have a place for declarations of new variables so you should use it. Only declare the iterator outside of the loop if you need it after the loop (eg. to check if the loop terminated early).

              > Need to re-read chapter 3 about debugging
              This was just about building in debug mode. When you build in debug mode, identifiers and file mappings are preserved, so debuggers can resolve addresses to your source code. In release mode, all such information is stripped, because your computer doesn't need it. -g is the flag you need.

              • Anastasia

                > `for`-loops have a place for declarations of new variables so you should use it.
                I mean having everything in the header, but splitting it in several lines (like you did).

                > -g is the flag you need.
                Thanks a lot!

                • cpp-wise yes. The only thing preventing you from doing so could be your style convention. If it enforces a line length limit, it might also tell you when and where to split lines. If there is no rule for line splitting, split wherever you please as long as it's readable and consistent.

                  • Anastasia

                    Ok, I'll try to do it this way next time, if necessary.

                    Thank you once again for taking the time to explain and clarify everything, I learned so much!

  • noobmaster

    For solution number 3, why can't we initialize int *m_array with new int[m_length] instead of nullPtr(line 8) and then comment out line 16 and 24 ?

  • DecSco

    Here's an option for the operator+ without casting to double:

    Feedback welcome!

    • - Don't use `std::int*_t` it's not guaranteed to exist.
      - Enable compiler warnings, read them, fix them.
      - Line 16, 19, 24: Brace initialization.

      You can use a sign variable to combine the 2 ifs

      Your version is easier to understand, but I spent more time on this code than I'd like to admit so I might as well share it.

      • DecSco

        Thanks! I somehow completely forgot that the return statement itself creates an object!
        The quiz instructed to use those int*_t types - the solution differs from that already, though. What about int_least8_t? Same issue?
        I wanted to write code without using abs(), and I don't want to admit how long it took me, either :D

  • DecSco

    The initialisation for the FixedPoint2 constructor from a double can (should?) be moved to an initialiser list:

  • DecSco

    Does it make any difference, precision wise, which of the member variables you cast to a double?

    I thought that, because you lose precision for high integer numbers when converting them to a double, it would be better to cast the lower number to a double. But then I thought that probably, the floating point division will produce the same result either way, because both numbers are converted, right?

Leave a Comment

Put all code inside code tags: [code]your code here[/code]