Lab #2

Protected members, function overloading and new operators for pvector

Cristiano De Michele

Today

  1. Introduction to C++ (part II) — protected members (example N4) and function overloading (example N5).
  2. Lab #1: the solution — the methods of pvector you wrote last time.
  3. Task: new constructor and operators for pvector — a constructor with an initializer_list, +=, -=, scalar product, norm(), vector times and divided by a scalar, *=, /=, == and the cross product ^.

Along the way we go back to some ideas of Lab #1 that the new methods use all the time: const methods, the pointer this and the object *this, returning by reference, const references, temporary objects.

Part 1
Introduction to C++ (part II)

Where we left off

  • Inheritance (N2): class vecder: public myvec inherits all the members of myvec and adds show(). But show() cannot read v[i]: v is private in myvec, so it has to go through the public methods get() and length().
  • Operator overloading (N3): = and + written as methods of vecdder; v3 = v1 + v2 is v3.operator=(v1.operator+(v2)).

Today:

  • a third access level, between private and public: protected;
  • function overloading: several methods with the same name, as we did for the operators, and what happens to it in a derived class.

Three access levels

Who can use a member, according to where it is declared:

member declared methods of the class methods of a derived class outside (e.g. main)
private yes no no
protected yes yes no
public yes yes yes
  • In a class the default is private (in a struct it is public).
  • protected: hidden from the users of the class, visible to who extends it.
  • class dercl: public basecl (public inheritance): the inherited members keep their level. The private ones are in the derived object too, but it cannot touch them.
  • Data private (or protected if derived classes need them), methods public. With v protected in myvec, vecder::show() (N2) could use v[i] directly.

Example N4: protected.hpp

examples/N4_protected_members/protected.hpp

#include<iostream>
class basecl
{
private: // this is redundant
  // private members
  int a;
  int fa(void)
    {
      return a;
    }
protected:
  int b; // allocated int
  int fb(void)
    {
      // fb() returns the value of member b
      return b;
    }
public:
  int c;
  int fc(void)
    {
      // fc() returns the value of member b
      return b;
    }
};

class dercl: public basecl
{
  int g(void)
    {
      fc();// this method is public
      fb(); // this method protected
      return b;
    }
};
  • basecl has one member per level: a, fa() private; b, fb() protected; c, fc() public.
  • dercl::g() calls fc() and fb() and reads b; with return a; it stops: error: 'int basecl::a' is private within this context.
  • g() has no access specifier in dercl: it is private, the default of a class.

Example N4: protected.cpp

examples/N4_protected_members/protected.cpp

#include "./protected.hpp"
int main(int argc, char** argv)
{
  // class instantiation
  dercl v;
  v.c = 10;
  v.fc(); // we can use only public members of a class (not protected or private ones!)
}
g++ -std=c++17 -Wall -o protected protected.cpp && ./protected    # prints nothing
  • main sees only the public members of dercl: v.c, v.fc() (inherited from basecl). Try the other ones in place of v.fc();:
v.b = 1;   // error: 'int basecl::b' is protected within this context
v.fb();    // error: 'int basecl::fb()' is protected within this context
v.g();     // error: 'int dercl::g()' is private within this context

Function overloading

Several functions (or methods) with the same name and different parameters (number or types). The compiler chooses, at compile time, from the types of the arguments:

void print(int i)          { std::cout << "int "    << i << "\n"; }
void print(double x)       { std::cout << "double " << x << "\n"; }
void print(std::string s)  { std::cout << "string " << s << "\n"; }

print(3);       // int 3
print(3.5);     // double 3.5
print("abc");   // string abc
  • The return type does not count: int f(int) and double f(int) together give error: ambiguating new declaration of 'double f(int)'.
  • In C every function needs its own name (abs, labs, fabs); in C++ one name is enough, std::abs, overloaded for int, long, double, …
  • We already did it with the operators: + exists for int, for double and, since Lab #1, for pvector. Today we overload the constructor of pvector and operator* (two versions).

Overloading in a derived class

class vecder { public: void show(void) const; ... };    // in the base class (N2)

class vecddder: public vecdder                          // N5, derives from N3
{
public:
  void show(std::string txt) const;                     // a new show(...)
};

vecddder v1;
v1.show("v3:");   // ok
v1.show();        // error: no matching function for call to 'vecddder::show()'
  • Name hiding: a method declared in a derived class hides all the methods with the same name of the base classes, whatever their parameters. The compiler looks for show in vecddder, finds it, and does not look further up.
  • using vecder::show; in the derived class brings the versions of the base back into play: then all the show are overloads of each other.
  • vecder::show(): the scope operator :: calls the version of a given class.

Example N5: func_overloading.hpp

examples/N5_func_overloading/func_overloading.hpp

#include "../N3_overloading/overloading.hpp"
class vecddder: public vecdder
{
public:
  // For overloading, we need to explicitly say that we want to use show
  // of base class method as public method of derived class too
  using vecder::show;
  // overloading of show method which now has an argument (a string)
  void show(std::string txt) const
    {
      std::cout << txt;
      vecder::show();
    }
  void show(int a) const
    {
      std::string s;
      // we can add string 
      // and convert integer or float to strings
      s = "v" + std::to_string(a) + "=";
      show(s);
    }

};
  • vecddder (three d) derives from vecdder (N3), which derives from vecder (N2), where show() is defined: three versions of show, with no argument (from vecder, thanks to the using of line 7), with a std::string, with an int.
  • show(int) builds the string "v1=": std::to_string(a) turns a number into a std::string, and + concatenates strings. Then it calls the string version.

Example N5: func_overloading.cpp

examples/N5_func_overloading/func_overloading.cpp

#include "./func_overloading.hpp"
int main(void)
{
  vecddder v1;
  for (int i = 0; i < 3; ++i) {
    v1.set(i,i); // initialize elements of vector v1
  }
  v1.show(); // show content
  v1.show("v3:"); // show content using overloaded method show() (see func_overloading.hpp file)
  v1.show(1);
  return 0;
}
g++ -std=c++17 -Wall -o func_overloading \
    func_overloading.cpp
./func_overloading
(0,1,2)
v3:(0,1,2)
v1=(0,1,2)
  • Three calls, three different methods, chosen from the argument: none, "v3:" (a C string, converted to std::string), 1 (an int).
  • Remove line 7 of the header (using vecder::show;): v1.show(); does not compile any more, error: no matching function for call to 'vecddder::show()'.

Part 2
Lab #1: the solution

Constructor, destructor, show()

project/pvector.hpp

  pvector() // void constructor
    {
      // initialize the vector
      for (int i=0; i < NT; i++)
        v[i]=0;
      //std::cout << "void constructor\n";
    }

  ~pvector() // destructor
    {
      // nothing to do
      //std::cout << "void destructor\n";
    }
  // Note this method does not change data of object hence it is declared "const"
  void show(std::string s="") const // default argument
    {
      std::cout << s << "("; 
      for (int i=0; i < NT; i++)
        {
          std::cout << v[i];
          if (i < NT-1)
            std::cout << ",";
        }
      std::cout << ")\n";
    }
  • pvector() sets all the NT components to 0: every pvector starts in a known state. Uncomment the std::cout lines to see when the vectors are created and destroyed.
  • The destructor has nothing to do: v is a fixed-size array, it disappears with the object.
  • show() is const (it only reads) and loops over NT.

sum() and operator+

project/pvector.hpp

  // method to sum two vectors
  pvector sum(const pvector &V2) const
    {
      pvector VT;
      for (int i=0; i < NT; i++)
        {
          VT.v[i] = v[i] + V2.v[i];
        }
      return VT;
    }
  // addition operator overloading
  // Note this method does not change data of object hence it is declared "const"
  pvector operator+(const pvector& V2) const
    {
      pvector VT;
      for (int i=0; i < NT; i++)
        {
          VT.v[i] = v[i] + V2.v[i];
        }
      return VT;
    } 
  • The recipe of Lab #1: a local VT, filled component by component, returned by value.
  • VT.v[i] is legal: inside a method of pvector the private members of any pvector are accessible.
  • operator+ repeats the loop of sum(); it could just as well be return sum(V2);.

operator-

project/pvector.hpp

  // subtraction operator overloading
  // Note this method does not change data of object hence it is declared "const"
  pvector operator-(const pvector& V2) const
    {
      pvector VT;
      for (int i=0; i < NT; i++)
        {
          VT.v[i] = v[i] - V2.v[i];
          // equivalently:
          // VT.v[i] = (*this).v[i] - V2.v[i];
        }
      return VT;
    } 
  • Same recipe, with -.
  • The comment in the loop: v[i] is a shorthand for (*this).v[i], the components of the object on the left of -; V2.v[i] are those of the argument.

get(), set() and the test

project/pvector.hpp

  // get value of a vector element
  // Note this method does not change data of object hence it is declared "const"
  ntype get(int i) const
    {
      return v[i];
    }

  // set value of vector element
  ntype set(int i, ntype val) 
    {
      return v[i]=val;
    }
  • get() is const, set() is not: it changes the object.
  • return v[i]=val;: as in C, an assignment is an expression, and its value is the value assigned. set() assigns and returns val in one line.
  • With the main of Lab #1 (pvector.cpp) the output is the one expected:
C=(0,0,0)
A=(1,2,3)
B=(0.5,-1,2)
A(1)=2
A+B=(1.5,1,5)
A-B=(0.5,3,1)
A.sum(B)=(1.5,1,5)
E=(1,2,3)
PC=(0,0,0)

Part 3
New tasks for pvector

The task

Download again project_classes.zip (course page; unzip it elsewhere, not over your own file): the new project/pvector.hpp contains the methods of Lab #1, implemented (Part 2), and the skeleton of today’s methods. The TODO list is updated:

// ...
// [X] get( ) (get i-th element)
// [X] set( , ) (set i-th element)
// [*] constructor with overloading->initializer_list
// [*] overloading of += and -= operators with vectors
// [*] overloading of * operator for scalar product (vector times vector)
// [*] norm() norm of vector, implemented by using overloaded operator "*"
// [*] overloading of * operator for "vector times scalar" product
// [*] overloading of / operator for "vector divided by scalar" product
// [*] overloading of *= and /= (with scalars)
// [*] overloading of operator == (check whether two vectors are equal)
// [*] overloading of ^ operator for cross product 
// [ ] overloading of (...) operator () to use vector as C arrays, e.g. v(0)=1
// ...

The skeleton gives comments, signatures and empty bodies: fill them. Until then it compiles with 10 warnings no return statement in function returning non-void, as in Lab #1. The next slides explain the ideas behind the signatures; then a main to test everything.

An operator is a method

project/pvector.hpp: assign() and operator= do the same thing, only the name changes.

  // since assign operator returns pvector objects by reference:
  // Note this method changes data of object hence it is *not* declared "const"
  pvector& assign( const pvector& P1)
    {
      for (int i=0; i < NT; i++)
        v[i] = P1.v[i];
      return (*this);
    }
  pvector& operator=(const pvector& V2)
    {
      for (int i=0; i < NT; i++)
        v[i] = V2.v[i];
      return (*this);

    }

The compiler translates every operator into a call of a method of the left operand (*this inside the method), with the right operand as argument:

A = B;          // A.operator=(B);                like A.assign(B);
A = B = C;      // A.operator=(B.operator=(C));   like A.assign(B.assign(C));
C = A + B;      // C.operator=(A.operator+(B));
D += B;         // D.operator+=(B);
s = A*B;        // s = A.operator*(B);
D = A*2.0;      // D = A.operator*(2.0);
if (A == B)     // if (A.operator==(B))

The long forms compile too: try A.assign(B);, A.operator=(C);, D = A.operator+(B);.

Consequences

  • Inside operator+, v[i] are the components of A (*this), V2.v[i] those of B.
  • The const at the end of a method protects the left operand: A + B, A*B, A == B are const methods, A = B and D += B are not. The right operand is protected by the const in const pvector& V2.
  • The assignment operators return the left operand itself (return (*this);, type pvector&): this is why A = B = C works. The others return a new object.
  • A*B and A*2.0 are two different methods of A, operator*(const pvector&) and operator*(ntype): function overloading (Part 1), chosen from the type of the right operand.
  • 2.0*A would be 2.0.operator*(A): double is not a class and has no methods, so it does not compile. In a next lab: a function outside the class (friend).
  • Precedence and number of operands stay those of C: the compiler applies them first, then turns A ^ B into A.operator^(B) (see the cross product).

Back to this and *this

A.norm();          // inside norm(), this == &A
D += B;            // D.operator+=(B): inside, this == &D
  • Every method receives an implicit argument: this, a pointer to the object it is called on. Writing v[i] inside a method means this->v[i], that is (*this).v[i]: the components of that object.
  • *this is the object itself (* applied to a pointer, as in C). You need it when a method must use or return the whole object:
    • return (*this); in operator= and today in +=, -=, *=, /=;
    • (*this)*(*this) in norm(): the scalar product of the object with itself.
  • V2.v[i] are the components of the argument, v[i] those of *this.

Back to const methods

ntype norm(void) const      // const after the parameters: *this is read-only
  • In a const method this points to a const pvector: the method can only read the object. Assigning v[i] inside it is a compile error.
  • Only const methods can be called on a const object or through a const reference. A norm() without const, used on a const pvector& V: error: passing 'const pvector' as 'this' argument discards qualifiers.
  • So: const every method that does not change the object.
today, const today, not const
A*B, norm(), A*s, A/s, A==B, A^B: they compute a new value +=, -=, *=, /=: they change the object on the left

Arguments by const reference

pvector& operator+=(const pvector& V2)    // vectors: by const reference
pvector& operator*=(ntype s)              // numbers: by value
  • const pvector&: no copy of the vector (&), and the method promises not to change it (const). Always use it for vector arguments.
  • A number (ntype s) is passed by value: copying a double costs nothing.
  • A const reference accepts temporary objects too: in A*(B+C) the argument is B+C, a vector with no name that lives only in that expression. With a non-const pvector& it does not compile: error: cannot bind non-const lvalue reference of type 'pvector&' to an rvalue of type 'pvector'.

Returning by value or by reference

returns what when
pvector (by value) a new vector, copied to the caller +, -, A*s, A/s, ^: the result is a local VT
pvector& (by reference) the object itself, no copy =, +=, -=, *=, /=: return (*this);
ntype, bool a number A*B, norm(), ==
  • By reference only something that survives the method: *this exists before and after the call. A local VT dies at the end of the method: a reference to it points to a dead object (Lab #1).
  • += returns pvector&, like operator=, so (D += B) *= 2.0; changes D twice. If it returned pvector (by value), *= 2.0 would act on a copy of D, and D would only get += B.

Temporary objects: + vs +=

The operators that are not assignments (+, -, *, /, ^) must not change their operands: A + B changes neither A nor B, so the result has to go into a new object, a temporary:

D = A + B + C;   // A+B -> temporary T1;  T1+C -> temporary T2;  D = T2
                 // then T1 and T2 are destroyed
D = A;           // the same result
D += B;          // no temporary: D changes in place
D += C;
  • Each temporary means a constructor, a loop to fill it, a copy and a destructor. For 3 components it is nothing; with \(10^5\) particles and \(10^6\) steps it adds up.
  • In the simulation loops prefer the assignment operators, where you can: r += v*dt; (one temporary, v*dt) rather than r = r + v*dt; (two).

operator+=: the model

// D += B   is   D.operator+=(B)
pvector& operator+=(const pvector& V2)   // not const: it changes *this
  {
    for (int i=0; i < NT; i++)
      {
        v[i] += V2.v[i];                 // the components of D
      }
    return (*this);                      // D itself, by reference
  }
  • No local vector: the result is written directly into the object.

  • operator-= is the same, with -=.

  • operator*= and operator/= take a number:

    pvector& operator*=(ntype s)    // D *= 2.0
    pvector& operator/=(ntype s)    // D /= 2.0

A second constructor: initializer_list

Goal: give the components when the vector is created, with braces:

pvector A = {1.0, 2.0, 3.0};
pvector B{0.5, -1.0, 2.0};      // the same, without =
  • Overloading of the constructor: a class can have several constructors, with different parameters. pvector D; calls pvector(), the braces the new one (skeleton):

    pvector(std::initializer_list<ntype> L)  // overloading constructor
  • std::initializer_list<ntype>: the list of values written between braces, all of type ntype (header <initializer_list>, already included). It cannot be changed; L.size() is the number of values.

  • pvector() must stay: once a class has a constructor, the compiler does not generate the void one any more, and without it pvector D; gives error: no matching function for call to 'pvector::pvector()'.

Going through an initializer_list

The values of L are read with a range-based for (C++11):

for (ntype x: L)            // x takes, in turn, each value of the list
  std::cout << x << " ";    // {1.0, 2.0, 3.0} -> 1 2 3
  • Read it as “for each x in L”. Keep your own counter i for the component to fill.

  • The list may have a number of values different from NT: decide what to do. For example missing components set to 0, extra values ignored:

    pvector C = {4.0};          // (4,0,0)

    Never write beyond v[NT-1].

Scalar product and norm()

ntype operator*(const pvector& V2) const    // s = A*B    is   s = A.operator*(B)
ntype norm(void) const                      // |A| = sqrt(A*A)
  • \(\mathbf{A}\cdot\mathbf{B} = \sum_{i} A_i B_i\): it returns a number (ntype), not a vector. Accumulate the sum in a local ntype initialized to 0.
  • norm(), \(|\mathbf{A}| = \sqrt{\mathbf{A}\cdot\mathbf{A}}\), must use the operator just written: (*this)*(*this) is the scalar product of the object with itself. sqrt comes from <cmath>.
  • Both are const.

Vector times and divided by a scalar

pvector operator*(ntype s) const    // A*s     a new vector
pvector operator/(ntype s) const    // A/s     a new vector
  • The second operator* of the class: A*B (a pvector on the right) is the scalar product, A*2.0 the vector times scalar. A*2 works too: the int 2 is converted to ntype.
  • 2.0*A (a double on the left) does not compile: error: no match for 'operator*' (operand types are 'double' and 'pvector').
  • Dividing by 0 is not checked: the components become inf or nan.

operator==

bool operator==(const pvector& V2) const    // if (A == B) ...
  • bool: true only if all the components are equal. You can return as soon as you find two different ones. std::cout prints a bool as 1 or 0.
  • It compares floating point numbers exactly: with E = {0.1, 0.2, 0.3}, (E*3.0)/3.0 == E is false, because 0.1*3.0 is 0.30000000000000004 (std::cout with its default 6 digits shows 0.3, which hides it).
  • So == is good for vectors copied from each other, not for the results of computations. Those are compared within a tolerance: (A-B).norm() < 1e-10.

Cross product ^

pvector operator^(const pvector& V2) const   // C = A ^ B

\[\mathbf{A}\times\mathbf{B} = (A_1B_2 - A_2B_1,\; A_2B_0 - A_0B_2,\; A_0B_1 - A_1B_0)\]

  • It returns a new vector (local VT, by value), const.
  • It is defined only for NT equal to 3: otherwise print a message and stop the program with exit(1) (from <cstdlib>, already included).
  • Check: \(\mathbf{A}\times\mathbf{B}\) is orthogonal to \(\mathbf{A}\) and \(\mathbf{B}\), so (A^B)*A and (A^B)*B must be 0.

The precedence of ^

^ is the bitwise xor of C. Overloading changes what an operator does, not its precedence nor its number of operands: ^ comes after +, -, * and even ==.

D = A ^ B + C;      // it is  A ^ (B + C)
                    // g++ -Wall: warning: suggest parentheses around
                    //            arithmetic in operand of '^'
if (D == A ^ B)     // it is  (D == A) ^ B
                    // error: no match for 'operator^'
                    //        (operand types are 'bool' and 'pvector')

Always use parentheses: (A ^ B) + C, D == (A ^ B). The same for <<: std::cout << A == B; is (std::cout << A) == B, an error; write std::cout << (A == B);.

The main

A main that tests all the new methods (your pvector.cpp):

#include "./pvector.hpp"
int main(void)
{
  pvector A = {1.0, 2.0, 3.0};   // initializer_list constructor
  pvector B{0.5, -1.0, 2.0};     // the same, without =
  pvector C = {4.0};             // missing components: 0
  pvector D;                     // void constructor
  A.show("A=");
  B.show("B=");
  C.show("C=");

  D = A;  D += B;    D.show("D=A, D+=B: ");
  D -= B;            D.show("D-=B: ");

  std::cout << "A*B=" << A*B << "\n";
  std::cout << "|A|=" << A.norm() << "\n";

  D = A*2.0;         D.show("A*2=");
  D = A/2.0;         D.show("A/2=");
  D = A;  D *= 3.0;  D.show("D=A, D*=3: ");
  D /= 3.0;          D.show("D/=3: ");

  std::cout << "A==B: " << (A == B) << "\n";
  std::cout << "A==D: " << (A == D) << "\n";

  D = A ^ B;         D.show("A^B=");
  std::cout << "(A^B)*A=" << (A^B)*A << "\n";
  return 0;
}

Compile, run, compare

cd ~/csm/project
g++ -std=c++17 -Wall -o pvector pvector.cpp
./pvector
A=(1,2,3)
B=(0.5,-1,2)
C=(4,0,0)
D=A, D+=B: (1.5,1,5)
D-=B: (1,2,3)
A*B=4.5
|A|=3.74166
A*2=(2,4,6)
A/2=(0.5,1,1.5)
D=A, D*=3: (3,6,9)
D/=3: (1,2,3)
A==B: 0
A==D: 1
A^B=(7,-0.5,-2)
(A^B)*A=0
  • (A^B)*A=0: the cross product is orthogonal to both vectors, a quick check of ^.
  • In std::cout << "A*B=" << A*B no parentheses are needed: * comes before <<.
  • Again: no warnings with -Wall, and everything must work with ntype = float.

Classic pitfalls

  • += returning pvector instead of pvector&: it compiles, D += B works, but (D += B) *= 2.0 changes a copy.
  • Missing const on operator*, norm(), ==, ^: they cannot be used on const objects nor on const pvector& arguments (... discards qualifiers).
  • Argument pvector& without const: temporaries like B+C are refused (cannot bind non-const lvalue reference ...).
  • Sum not initialized in the scalar product: ntype sp; contains garbage, ntype sp=0;.
  • Removing pvector() when adding the initializer_list constructor: pvector D; no longer compiles.
  • Writing past v[NT-1] with a list longer than NT.
  • Precedence of ^: always parentheses.
  • == on the results of computations: use a tolerance.

Checklist for today

Next lab: operator() to use a pvector as a C array (V(0)=1), “scalar times vector” with a friend function, rint(), element-wise products, << for output.