CSE333(12-14): C++ related

Today I learn C++ again…

This is a great language.

2020_12_05_0

Lecture 12 - C++ Constructor Insanity

Constructors

  • A class can have multiple constructors that differ in parameters, similar to JAVA.

  • C++ will automatically create a synthesized default constructor if you have no user-defined constructors.

  • If you define any constructors, C++ assumes you have defined all the ones you intend to be available and will not add any others.

  • Multiple Constructors (overloading)

  • Constructor with an initialization list

    • Data members in initializer list are initialized in the order they are defined in the class, not by the initialization list ordering
// constructor with an initialization list
Point::Point(const int x, const int y) : x_(x), y_(y) {
	std::cout << "Point constructed: (" << x_ << ",";
	std::cout << y_<< ")" << std::endl;
}

Copy Constructors

  • C++ has the notion of a copy constructor (cctor)
    • Used to create a new object as a copy of an existing object
    • Initializer lists can also be used in copy constructors (preferred)
Point::Point(const int x, const int y) : x_(x), y_(y) { }
// copy constructor
Point::Point(const Point& copyme) {  // param: alias to a const object
	x_ = copyme.x_;
	y_ = copyme.y_;
}
void foo() {
	Point x(1, 2); // invokes the 2-int-arguments constructor
	Point y(x); // invokes the copy constructor
				// could also be written as "Point y = x;"
}

The copy constructor is invoked if:

  1. Initialize an object from another object of the same type:
Point x; // default ctor
Point y(x); // copy ctor
Point z = y; // copy ctor
  1. Pass a non-reference object as a value parameter to a function:
void foo(Point x) { ... }
Point y; // default ctor
foo(y); // copy ctor
  1. Return a non-reference object value from a function:
Point foo() {
	Point y; // default ctor
	return y; // copy ctor
}

Compiler Optimization

The compiler sometimes uses a “return by value optimization” or “move semantics” to eliminate unnecessary copies

  • Sometimes you might not see a constructor get invoked when you might expect it
Point foo() {
	Point y; // default ctor
	return y; // copy ctor? optimized?
}
Point x(1, 2); // two-ints-argument ctor
Point y = x; // copy ctor
Point z = foo(); // copy ctor? optimized?

Synthesized Copy Constructor

If you don’t define your own copy constructor, C++ will synthesize one for you

  • It will do a shallow copy of all of the fields (i.e. member variables) of your class
  • Sometimes the right thing; sometimes the wrong thing

Assignment

  • Assignment != Construction
Point w; // default ctor
Point x(1, 2); // two-ints-argument ctor
Point y(x); // copy ctor
Point z = w; // copy ctor
y = x; // assignment operator
  • Overloading the “=” Operator
Point& Point::operator=(const Point& rhs) {
	if (this != &rhs) { // (1) always check against this
		x_ = rhs.x_;
		y_ = rhs.y_;
	}
	return *this; // (2) always return *this from op=
}
  • Synthesized Assignment Operator
    • If you don’t define the assignment operator, C++ will synthesize one for you

Destructors

C++ has the notion of a destructor (dtor):

  • Invoked automatically when a class instance is deleted, goes out of scope, etc. (even via exceptions or other causes!)
  • Place to put your cleanup code – free any dynamic storage or other resources owned by the object
  • Standard C++ idiom for managing dynamic resources • Slogan: “Resource Acquisition Is Initialization” (RAII)
Point::~Point() { // destructor
	// do any cleanup needed when a Point object goes away
	// (nothing to do here since we have no dynamic resources)
}

Lecture 13 - C++ Class Details, Heap

Class Details

Rule of Three

If you define any of:

  • Destructor
  • Copy Constructor
  • Assignment (operator=) Then you should normally define all three, can explicitly ask for default synthesized versions (C++11):
class Point {
	public:
		Point() = default; // the default ctor
		~Point() = default; // the default dtor
		Point(const Point& copyme) = default; // the default cctor
		Point& operator=(const Point& rhs) = default; // the default "="
		...
}

Dealing with the instanity

C++ style guide tip:

  • If possible, disable the copy constructor and assignment operator. C++11 has direct syntax to indicate this:
class Point {
	public:
		Point(const int x, const int y) : x_(x), y_(y) { } // ctor
		...
		Point(const Point& copyme) = delete; // declare cctor and "=" as
		Point& operator=(const Point& rhs) = delete; // as deleted (C++11)
	private:
	...
}; 

Point w; // compiler error (no default constructor)
Point x(1, 2); // OK!
Point y = w; // compiler error (no copy constructor)
y = x; // compiler error (no assignment operator)

If before C++11:

  • In pre-C++11 code the copy constructor and assignment were often disabled by making them private and not implementing them.

Example:

class Point {
	public:
		Point(const int x, const int y) : x_(x), y_(y) { } // ctor
		...
	private:
		Point(const Point& copyme); // disable cctor (no def.)
		Point& operator=(const Point& rhs); // disable "=" (no def.)
		...
};

Point w; // compiler error (no default constructor)
Point x(1, 2); // OK!
Point y = w; // compiler error (no copy constructor)
y = x; // compiler error (no assignment operator)

struct vs. class

In C, a struct can only contain data fields

  • Has no methods and all fields are always accessible
  • In struct foo, the foo is a “struct tag”, not an ordinary data type

In C++, struct and class are (nearly) the same!

  • Both define a new type (the struct or class name)
  • Both can have methods and member visibility (public/private/protected)
  • Only real (minor) difference: members are default public in a struct and default private in a class

Common style/usage convention:

  • Use struct for simple bundles of data
    • Convenience constructors can make sense though
  • Use class for abstractions with data + functions

Access Control

Access modifiers for members:

  • public: accessible to all parts of the program
  • private: accessible to the member functions of the class
    • Private to class, not object instances
  • protected: accessible to member functions of the class and any derived classes (subclasses – more to come, later)

Reminders:

  • Access modifiers apply to all members that follow until another access modifier is reached
  • If no access modifier is specified, struct members default to public and class members default to private

Nonmember Functions

“Nonmember functions” are just normal functions that happen to use some class

  • Called like a regular function instead of as a member of a class object instance
    • This gets a little weird when we talk about operators…
  • These do not have access to the class’ private members
  • can overload operators using both member functions and nonmember functions

friend Nonmember Functions

A class can give a nonmember function (or class) access to its nonpublic members by declaring it as a friend within its definition

  • friend function is not a class member, but has access privileges as if it were
  • friend functions are usually unnecessary if your class includes appropriate “getter” public functions

Complex.h:

class Complex {
	...
	friend std::istream& operator>>(std::istream& in, Complex& a);
	...
}; // class Complex

Complex.cc:

std::istream& operator>>(std::istream& in, Complex& a) {
	...
}

Namespaces

  • Each namespace is a separate scope
    • Useful for avoiding symbol collisions
  • Namespace definition
    • Creates a new namespace name if it did not exist, otherwise adds to the existing namespace, components (classes, functions, etc.) of a namespace can be defined in multiple source files

Classes vs. Namespaces

  • classes are not namespaces.
  • There are no instances/objects of a namespace; a namespace is just a group of logically-related things (classes, functions, etc.)
  • To access a member of a namespace, you must use the fully qualified name (i.e. nsp_name::member)
    • Unless you are using that namespace
    • You only used the fully qualified name of a class member when you are defining it outside of the scope of the class definition

Using the Heap

C++11 nullptr

  • NULL as a pointer value that references nothing

C++11 introduced a new literal for this: nullptr

  • New reserved word
  • Interchangeable with NULL for all practical purposes, but it has type T* for any/every T, and is not an integer value
    • Avoids funny edge cases (see C++ references for details)
    • Still can convert to/from integer 0 for tests, assignment, etc.
  • Advice: prefer nullptr in C++11 code
    • Though NULL will also be around for a long, long time

new / delete

To allocate on the heap using C++, you use the new keyword instead of malloc() from stdlib.h

  • You can use new to allocate an object (e.g. new Point)
    • Will execute appropriate constructor as part of object allocate/create
  • You can use new to allocate a primitive type (e.g. new int)

To deallocate a heap-allocated object or primitive, use the delete keyword instead of free() from stdlib.h

Don’t mix and match!

  • Never free() something allocated with new
  • Never delete something allocated with malloc()
  • Careful if you’re using a legacy C code library or module in C++

Example:

int* AllocateInt(int x) {
	int* heapy_int = new int;
	*heapy_int = x;
	return heapy_int;
}

Point* AllocatePoint(int x, int y) {
	Point* heapy_pt = new Point(x,y);
	return heapy_pt;
}

heappoint.cc:

#include "Point.h"
using namespace std;
... // definitions of AllocateInt() and AllocatePoint()
int main() {
	Point* x = AllocatePoint(1, 2);
	int* y = AllocateInt(3);
	cout << "x's x_ coord: " << x->get_x() << endl;
	cout << "y: " << y << ", *y: " << *y << endl;
	delete x;
	delete y;
	return 0;
}

Dynamically Allocated Arrays

To dynamically allocate an array:

type* name = new type[size];

To dynamically deallocate an array:

delete[] name;

It is an incorrect to use delete name; on an array

  • The compiler probably won’t catch this, though (!) because it can’t always tell if name* was allocated with new type[size] or new type; – Especially inside a function where a pointer parameter could point to a single item or an array and there’s no way to tell.
  • Result of wrong delete is undefined behavior

Arrays Example (primitive):

#include "Point.h"
using namespace std;

int main() {
	int stack_int;
	int* heap_int = new int;
	int* heap_init_int = new int(12);

	int stack_arr[10];
	int* heap_arr = new int[10];

	int* heap_init_arr = new int[10](); // uncommon usage
	int* heap_init_error = new int[10](12); // bad syntax
	...
	delete heap_int; //		ok
	delete heap_init_int; //	ok
	delete heap_arr; //		error - must be delete[]
	delete[] heap_init_arr; //	  ok
	return 0;
}

Arrays Example (class objects):

#include "Point.h"
using namespace std;

int main() {
	...
	Point stack_point(1, 2);
	Point* heap_point = new Point(1, 2);
	Point* err_pt_arr = new Point[10];// bug-no Point() ctr
	Point* err2_pt_arr = new Point[10](1,2); // bad syntax
	...
	delete heap_point;
	...
	return 0;
}

malloc vs. new

malloc() new
What is it? a function an operator or keyword
How often used(in C)? often never
How often used(in C++)? rarely often
Allocated memory for anything arrays, structs, objects, primitives
Returns a void* (should be cast) appropriate pointer type(doesn’t need a cast)
When out of memory returns NULL throws an exception
Deallocating free() delete or delete[]

Str Class Walkthrough

#include <iostream>
using namespace std;
class Str {
	public:
		Str(); // default ctor
		Str(const char* s); // c-string ctor
		Str(const Str& s); // copy ctor
		~Str(); // dtor
		int length() const; // return length of string
		char* c_str() const; // return a copy of st_
		void append(const Str& s);
		Str& operator=(const Str& s); // string assignment
		friend std::ostream& operator<<(std::ostream& out, const Str& s);
	private:
		char* st_; // c-string on heap (terminated by '\0')
};  // class Str

See:

  • Str.h
  • Str.cc
  • strtest.cc

Lecture 14 - C++ Templates

What we’d prefer to do is write ‘generic code’

  • Code that is type-independent
  • Code that is compile-type polymorphic across types

C++ has the notion of templates: A function or class that accepts a type as a parameter

  • You define the function or class once in a type-agnostic way
  • When you invoke the function or instantiate the class, you specify (one or more) types or values as arguments to it

At compile-time, the compiler will generate the “specialized” code from your template using the types you provided

  • Your template definition is NOT runnable code
  • Code is only generated if you use your template
  • Code is specialized for the specific types of data used in the template instance (e.g.: code for < on ints differs from code for < on strings)

Example

#include <iostream>
#include <string>
// returns 0 if equal, 1 if value1 is bigger, -1 otherwise
template <typename T> // <...> can also be written <class T>
int compare(const T &value1, const T &value2) {
	if (value1 < value2) return -1;
	if (value2 < value1) return 1;
	return 0;
}
int main(int argc, char **argv) {
	std::string h("hello"), w("world");
	std::cout << compare<int>(10, 20) << std::endl;
	std::cout << compare<std::string>(h, w) << std::endl;
	std::cout << compare<double>(50.5, 50.6) << std::endl;
	return 0;
}

Letting the compiler infer the types:

int main(int argc, char **argv) {
std::string h("hello"), w("world");
std::cout << compare(10, 20) << std::endl; // ok
std::cout << compare(h, w) << std::endl; // ok
std::cout << compare("Hello", "World") << std::endl; // hm…?
return 0;
}

Pair Class Definition

Pair.h:

#ifndef _PAIR_H_
#define _PAIR_H_
template <typename Thing> class Pair {
	public:
		Pair() { };
		Thing get_first() const { return first_; }
		Thing get_second() const { return second_; }
		void set_first(Thing &copyme);
		void set_second(Thing &copyme);
		void Swap();
	private:
		Thing first_, second_;
};
#include "Pair.cc" // or (better?) put entire template def here
#endif // _PAIR_H_

Pair.cc:

template <typename Thing>
	void Pair<Thing>::set_first(Thing &copyme) {
	first_ = copyme;
}
template <typename Thing>
	void Pair<Thing>::set_second(Thing &copyme) {
	second_ = copyme;
}
template <typename Thing>
void Pair<Thing>::Swap() {
	Thing tmp = first_;
	first_ = second_;
	second_ = tmp;
}
template <typename T>
std::ostream &operator<<(std::ostream &out, const Pair<T>& p) {
	return out << "Pair(" << p.get_first() << ", "
	<< p.get_second() << ")";
}

Know C++ more…..

References