lecture-03
lecture-03
Mike Spertus
[email protected]
SORTING PERFORMANCE
Advanced: Were you surprised by the
different median performances?
int main() {
int x = 2;
cout << x << endl; // prints 2
f(x); // print’s f’s x is 3
cout << x << endl; // prints 2
g(x); // print’s g’s x is 4
cout << x << endl; // prints 4
h(x); // Illegal! x may be used again
h(5*5); // print’s h’s x is 5
return 0;
}
Why does only C++ make you
specify how an object is passed?
⚫ Most languages only have only one way of
passing arguments
⚫ In C, arguments are always passed by value
⚫ The function always gets its own copy
⚫ In Java, arguments are always passed by
reference
⚫ The function sees the same object the caller passed
⚫ Exception: built-in numeric types like int are passed by
value
⚫ As usual, C++ let’s you choose which you want
(and adds a new “move” mode)
Passing by value:
pros and cons
⚫ Pro: It is very safe to call a function that takes its
arguments by value because the function you called can’t
actually see the object you passed it
⚫ Con: Copying an object may be very expensive
⚫ Imagine you are copying a map with a million key-value pairs
⚫ Con: Doesn’t work with inheritance/OO
⚫ If I copy an Animal, but it’s really a Gorilla, my copy will only
have Animal fields (and virtual functions may try to reference
non-existent Gorilla fields, crashing my program!)
⚫ Con: Sometimes I may want to modify the caller’s object.
E.g., they want me to clean the data in a vector.
Passing by reference:
pros and cons
⚫ Con: It is dangerous to call a function that takes its arguments by
reference because it may unexpectedly modify your object
⚫ Pro: You can fix this by passing the argument by “const
reference”, which says the function is not allowed to modify it
(e.g., by calling a non-const method on it)
⚫ void f(int const &i) { i = 3; // illegal: i is const}
⚫ You can say either X const or const X. Which is better? HW!
⚫ Pro: Efficient, since object doesn’t need to be copied
⚫ Pro: Works with inheritance/OO
⚫ No slicing because I am working with the original object
⚫ Pro: I can modify the object if desired and appropriate
⚫ Con: Managing memory is difficult
⚫ If my object has many owners, how do I now when it is safe to delete
⚫ We will learn best practices around this later
Passing by move
Pros and Cons
⚫ Sometimes you need your own copy of an object
(by value!) but the caller doesn’t need it any
more, so it seems wasteful to copy the object
⚫ For example, if it is a binary tree, you
could just copy the
root and take over
ownership of the leaves
⚫ As we will see later, this can make
algorithms like sort as much as
100x faster
So what should our Triangle
printer look like?
⚫ In the code, it is
⚫ ostream&
operator<<(ostream &os, Triangle triangle)
⚫ What is best?
⚫ We want to print to the original ostream, so better not copy it
⚫ In fact, the compiler won’t even let us make a copy of an ostream as
we will see)
⚫ Using ostream& is indeed best here
⚫ Copying a (possibly big) Triangle just so our printing function
can see it seems wasteful, so we would be better off with a
reference
⚫ But we also want to be able to print Triangles that we don’t have
the right to modify, which leads us to:
⚫ ostream&
operator<<(ostream &os, Triangle const &triangle)
CONSTRUCTORS AND
DESTRUCTORS
How does C++ know how to
copy and move an object?
⚫ A class’ “copy constructor” explains how to copy the class
⚫ struct X {
X(X const &x) { /* how to copy */ }
};
⚫ Not to worry. If you don’t specify a copy constructor, the compiler will
automatically generate one for you
⚫ It calls the copy constructors of all of the base classes and members in the same order
we discussed last week
⚫ Still, sometimes you need to explain how to copy your class, so the ability to
customize is good
⚫ If you are familiar with clone() methods in Java, this is very similar
⚫ Later this quarter, you will write a binary tree that does a deep copy
⚫ Sometimes, you don’t want a class to be copyable
⚫ struct X { X(X const &x) = delete; // Don’t copy X objects };
⚫ (That is how ostreams prevent themselves from being copied)
⚫ Move construction is handled analogously
⚫ struct X { X(X const &&x) { /* how to move */ } };
invoking a constructor to
create an object
// construction.cpp on chalk
#include<initializer_list>
using std::initializer_list
struct A {
A(int i, int j = 0); // #1
explicit A(int i); // #2
};
struct B {
B(int i, int j); // #3
B(initializer_list<int> l); // #4
};
int main() {
A a0 = 3; // A(3, 0) #1
A a1(3); // A(3) #2
A a1{3}; // A(3) #2. Uniform init: best practice but
B b0(3, 5); // #3
B b1 = { 3, 5}; // #4
B b2{3, 5}; // #4 Initializer list is preferred
}
Constructor Signatures
struct A {
A(int _i = 0) : i(_i) {}
// Alternate
A(int _i = 0) : i{_i} {}
// Alternate 2
// A(int _i = 0) { i = _i; }
int i;
};
Implicit conversions
⚫ Built-in
⚫ int i = 780;
long l = i;
char c = 7;
char c = i; // No warning, but dangerous!
⚫ Polymorphism
⚫ Animal *ap = new Dog;
⚫ Animal a = Dog(); // Ill-formed! Slicing
⚫ User-defined
⚫ Constructors
⚫ Operator overloading
⚫ “Standard Conversions”
⚫ Defined in clause 4 of the standard
Constructors and typecasts
struct A {
A();
A(int i);
A(int i, string s);
explicit A(double d);
};
A a;
A a0(1, "foo");
A aa = { 1, "foo"};
A a1(7); // Calls A(int)
a1 = 77;// ok
A a3(5.4); // Calls A(double)
a3 = 5.5; // Calls A(int)!!
Type conversion operators
struct seven {
operator int() { return 7; }
};
struct A { A(int); }
int i = seven();
A a = 7;
A a = seven(); // Illegal, two user-
// defined conversions not allowed
Explicit conversions
⚫ Old-style C casts (Legal but bad!)
⚫ char *cp f(void *vp) { return (char *)vp; }
⚫ New template casting operators
⚫ static_cast<T>
⚫ Like C casts, but only makes conversions that are always valid. E.g, convert
one integral type to another (truncation may still occur).
⚫ dynamic_cast<T*>
⚫ Casts between pointer types. Can even cast a Base* to a Derived* but only
does the cast if the target object really is a Derived*.
⚫ Only works when the base class has a vtable (because the compiler adds a
secret virtual function that keeps track of the real run-time type of the object).
⚫ If the object is not really a T *, dynamic_cast<T*> returns nullptr;
⚫ reinterpret_cast<T*>
⚫ Does a bitwise reinterpretation between any two pointer types, even for
unrelated types. Never changes the raw address stored in the pointer. Also
can convert between integral and pointer types.
⚫ const_cast<T>
⚫ Can change constness or volatileness only
Copy constructors
⚫ Classes can have constructors that show
how to make copies.
⚫ Signature is T(T const &)
⚫ A default copy constructor is almost always
generated
⚫ Calls the copy constructors of all the base classes
and members in the order we will discuss
⚫ T(T const &) = delete;
Default constructor
⚫ The “No argument” constructor is called the default constructor
⚫ struct A {
A() : i{5} (); // Default constructor
int i;
};
A a; // Calls default constructor
⚫ If you don’t define any constructors in your class, the compiler will
generate a default constructor for you
⚫ struct B {
string s;
};
B b; // Call compiler-generated default constructor
⚫ If you define any constructors, the compiler will not generate a default
constructor
⚫ struct C {
C(double d) : d{d} {}
double d;
};
C c; // Ill-formed! C has no default constructor
Review: Order of construction
⚫ Virtual base classes first
⚫ Even if not immediate
⚫ First base class constructors are run in the order
they are declared
⚫ Next, member constructors are run in the order of
declaration
⚫ This is defined, but very complicated
⚫ Best practice: Don’t rely on it
⚫ Good place for a reminder: Best practice: don’t use virtual
functions in constructors
Constructor ordering
class A {
public:
A(int i) : y(i++), x(i++) {}
int x, y;
int f() { return x*y*y; }
};
⚫ What is A(2).f()?