Wee2 A
Wee2 A
Oriented Systems
Design Using C++
C OM P- 34 00 - 30 - R - 20 25W
W E E K 2 : C O N C E P T S ; P O I N T E R S , I T E R AT O R S , R A N G E S , O P E R A T O R S , A N D O O P
• Operator Overloading
• Overview
• Unary, As Free Function
• Unary, As Member Function
• Binary, As Free Function
• Binary, As Member Function
• Prefix and Postfix + and - Operators
• IOStream Operator Overloads
• int number1;
float number2, sum;
sum = number1 + number2;
NOTE: friend functions are free functions that have access to all protected and
private scoped names in a class declaring that function as its friend.
Operator Overloading: Unary, As Member Function (con’t)
17 int main()
18 {
19 foo a;
20 std::cin >> a;
21 std::cout << "read in: " << a << '\n';
22 }
Operator Overloading: IOStream Operator Overloads (con’t)
This version uses some unformatted I/O calls to check some characters to ensure input
is valid (and writes such out when outputting):
w02/op-overload-io2.cxx
1 #include <istream>
2 #include <ostream>
3 #include <iostream>
4 #include <string>
5
6 struct foo { int i; };
7
8 std::ostream& operator<<(std::ostream& os, foo const& f) // i.e., notice const&
9 {
10 os << '[' << f.i << ']';
11 return os;
12 }
13
14 std::istream& operator>>(std::istream& is, foo& f) // i.e., notice & and no const
15 {
16 if (is.get() != '[')
Operator Overloading: IOStream Operator Overloads (con’t)
17 {
18 is.unget(); // put character read in back in stream buffer
19 is.setstate(std::ios_base::failbit); // fail the stream
20 return is;
21 }
22
23 int temp;
24 if (is >> temp && is.get() == ']')
25 f.i = temp;
26 else
27 is.setstate(std::ios_base::badbit); // make stream bad
28 return is;
29 }
30
31 int main()
32 {
33 foo a;
34 if (std::cin >> a)
35 std::cout << "read in: " << a << '\n';
36 }
Operator Overloading: IOStream Operator Overloads (con’t)
This version uses some inserted whitespace on output to make it easier to skip
whitespace when reading in data to process each component. This makes it
much easier to write I/O code to have some more extra formatting:
w02/op-overload-io3.cxx
1 #include <istream>
2 #include <ostream>
3 #include <iostream>
4 #include <string>
5
6 struct foo { int i; };
7
8 std::ostream& operator<<(std::ostream& os, foo const& f) // i.e., notice const&
9 {
10 os << "foo{ " << f.i << " }";
11 return os;
12 }
13
14 std::istream& operator>>(std::istream& is, foo& f) // i.e., notice & and no const
15 {
Operator Overloading: IOStream Operator Overloads (con’t)
16 std::istream::sentry s(is, true); // true ensures whitespace is skipped
17 if (s) // i.e., if the stream is in a good state
18 {
19 std::string before, after;
20 int temp;
21 if ((is >> before >> temp >> after) && before == "foo{" && after == "}")
22 f.i = temp; // set f since temp was properly read in
23 else
24 is.setstate(std::ios_base::badbit); // temp not properly read in; make stream bad
25 }
26 return is;
27 }
28
29 int main()
30 {
31 foo a;
32 if (std::cin >> a)
33 std::cout << "read in: " << a << '\n';
34 }
Outline
2 Relational Operators
Before C++20
C++20
Relational Orderings
Relational Operators: Before C++20
w02/rel-before-cxx20.cxx
1 // for some user-defined type, e.g.,...
2 struct foo { int i; };
3 // one needs to write all relevant/needed relational operator overloads which
4 // must return bool and need const& parameters, e.g.,
5 bool operator==(foo const& a, foo const& b) { return a.i == b.i; }
6 bool operator!=(foo const& a, foo const& b) { return a.i != b.i; }
7 bool operator <(foo const& a, foo const& b) { return a.i < b.i; }
8 bool operator<=(foo const& a, foo const& b) { return a.i <= b.i; }
9 bool operator>=(foo const& a, foo const& b) { return a.i >= b.i; }
10 bool operator >(foo const& a, foo const& b) { return a.i > b.i; }
11
12 int main()
13 {
14 foo a{1}, b{2};
15 if (a < b) { /* ... */ }
16 }
i.e., one had to write each and every desired relational operator manually.
Relational Operators: C++20
w02/rel-since-cxx20.cxx
1 #include <compare> // needed for std::*_ordering definitions
2
3 // for some user-defined type, e.g.,...
4 struct foo
5 {
6 int i;
7
8 // One can instead write a friend operator<=>(const&, const&) "= default"
9 // definition which will (lexicographically) define <=>, ==, !=, <, <=, >=,
10 // and >
11 friend constexpr auto operator<=>(foo const&, foo const&) noexcept = default;
12 };
13
14 int main()
15 {
16 foo a{1}, b{2};
17 if (a < b) { /* ... */ }
18 }
Relational Operators: C++20 (con’t)
In C++20, the language’s treatment of all of the relational operators was significantly
changed, and, a new three-way-operator, operator <=>, was introduced.
Before C++20:
• one had to write each and every relational operator for a type to support
all desired comparisons
• e.g., any/all of operator==, operator!=, operator<, operator<=,
operator>=, operator>
• such definitions were, clearly, based on either or both operator== and
operator< definitions
• leading to a more code clutter and maintenance
• leading to unintented mistakes/bugs
Relational Operators: C++20 (con’t)
Since C++20:
• If there is a definition of operator== then the compiler will generate a
definition of operator!= that performs the negation of operator==
• This assumes the user did not write a definition for operator!=.
• in cases where the arguments passed to operator== or operator<=> are
not the same, the compiler will automatically generate the symmetric
variants of either/both of these operators
• This assumes the user did not write explicit symmetric definition(s).
• if operator<=> is = defaulted, then such implies a compiler-generated
= defaulted operator==
• If the user does not explicitly provide a operator== definition.
• The reverse is not true: if operator<=> is not defaulted, then the compiler will not
generate operator==.
Relational Operators: C++20 (con’t)
20 }
21 };
22
23 int main()
24 {
25 foo a{1}, b{2};
26 std::cout << (a == b) << (a != b) << (a < b) << (a <= b) << (a >= b) << (a > b) << '\n'
27 // besides directly comparing ordering values, orderings can be compared
28 // relationally to 0, e.g.,
29 << ((a <=> b) == 0) << ' ' << ((a <=> b) < 0) << '\n';
30 }
Relational Operators: Relational Orderings
4 Standard Attributes
[ [ de p re cate d ] ]
[[fallthrough]]
[ [ l i k e l y ] ] and [ [ u n l i k e l y ] ]
[ [ ma yb e_ u nu s ed ] ]
[[ nodis ca rd]]
[[noreturn]]
[ [no_ un iq ue_ add ress ]]
Standard Attributes: [[deprecated]]
This attribute indicates the name/entity being declared is permitted but is discouraged
for some reason, e.g., it is expected to be removed/significantly changed later.
This attribute has two forms:
• [[deprecated]]
• [[deprecated("reason")]]
w02/deprecated.cxx
1 [[deprecated]] void foo() { }
2 [[deprecated("bar() will be removed in next release")]] void bar() { }
3 void foo(int) { }
4 void bar(int) { }
Standard Attributes: [ [ f a l l t h r o u g h ] ]
This) case label intattribute indicates that code falls through from the previous
(switcho the next, and, informs the compiler not to issue a warning.
w02/fallthrough.cxx
1 #include <iostream>
2 int main()
3 {
if (int i; std::cin >> i)
4
{
5
switch (i)
6
{
7
case 1:
8
case 2:
9
std::cout << "1 or 2\n";
10
[[fallthrough]];
11
case 3: // no warning given here
12
std::cout << "3\n";
13
}
14
}
15
16 }
Standard Attributes: [[likely]] and [[unlikely]]
w02/likely-unlikely2.cxx
1 constexpr unsigned long long fib(unsigned long long n) noexcept
2 {
3 if (n == 0) [[unlikely]]
4 return 0;
5 else if (n == 1) [[unlikely]]
6 return 1;
7 else [[likely]]
8 return fib(n-1) + fib(n-2);
9 }
10
11 constexpr unsigned long long factorial(unsigned long long n) noexcept
12 {
13 unsigned long long retval{1};
14 for (; n > 1; --n) [[likely]]
15 retval *= n;
16 return retval;
17 }
Standard Attributes: [[maybe_unused]]
This attribute informs the compiler that the function does not return, e.g.,
w02/noreturn.cxx
1 #include <cstdlib>
2 [[noreturn]] void foo() { throw "something"; }
3 [[noreturn]] void bar() { for (;;) ; }
4 [[noreturn]] void quit() { std::exit(10); }
Standard Attributes: [ [ n o r e t u r n ] ] (con’t)
6 A Review Of Pointers
A Review Of Pointers
In C and C++ array and pointer syntax are equivalent in this sense:
• array[index] ≡ *(array+index)
• array is an array or a pointer
• index is an integer
Applying the address-of operator, &, to both sides yields this equivalence:
• &array[index] ≡ array+index
A Review Of Pointers (con’t)
More examples:
• Given int i=3; int *p=&i; then p[0] = 2; is the same as writing *p = 2;.
• Given int ar[5]; then *(ar+3) = 2; is the same as writing ar[3] = 2;.
• Given int ar[5]; then ar+3 is the same as writing &ar[3].
A Review Of Pointers (con’t)
Know pointer values can only be meaningfully compared using <, <=, >=, and >
with:
• pointers pointing to the same underlying valid object having the same
type (subject to const-volatile-qualification rules), and,
• e.g., int ar[5]; int *p=ar; int *q=ar+3; bool b=(p < q);
• pointers pointing to the one-past-the-valid-object position for the same
underlying valid object and type.
• e.g., int i; int *p=&i; int *q=&i+1; bool b=(p < q);
• e.g., int ar[5]; int *p=ar; int *q=ar+5; bool b=(p < q);
A Review Of Pointers (con’t)
If pointers can be meaningfully compared using <, <=, >=, and > then they can be
meaningfully subtracted with the result being the type std::ptrdiff_t:
• e.g., int ar[5]; int *p=ar; int *q=ar+5; auto length=(p - q); // length == 5
With C ++ iterators:
• When subtracting two random-access iterators the result type is represented by
the iterator member typedef difference_type.
• Prefer using std::distance(from_pos,to_pos) over subtraction.
Outline
w02/p2i2r-1.cxx
1 #include <iostream>
2
3 int main()
4 {
5 using namespace std;
6
7 int ar[] = { 1, 2, 3, 4, 5 };
8
9 for (int i=0; i != 5; ++i)
10 cout << ar[i] << ' ';
11 cout << '\n';
12 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-2.cxx
1 #include <iostream>
2
3 int main()
4 {
5 using namespace std;
6
7 int ar[] = { 1, 2, 3, 4, 5 };
8 int *start = &ar[0]; // start points to a[0]
9 int *stop = start + 5; // start points to a[5] which is one past the last element
10
11 for (int *i=start; i != stop; ++i)
12 cout << *i << ' ';
13 cout << '\n';
14 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-3.cxx
1 #include <iostream>
2
3 int ar[] = { 1, 2, 3, 4, 5 };
4
5 using myiterator = int*; // same as: typedef int* myiterator;
6 myiterator start() { return &ar[0]; } // hard-coded to ar array for now
7 myiterator stop() { return start()+5; } // hard-coded to ar array for now
8
9 int main()
10 {
11 using namespace std;
12 for (myiterator i=start(); i != stop(); ++i) // now use myiterator, start(), and stop()
13 cout << *i << ' ';
14 cout << '\n';
15 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-4.cxx
1 #include <iostream>
2
3 int ar[] = { 1, 2, 3, 4, 5 };
4
5 using myiterator = int*;
6 myiterator mybegin() { return &ar[0]; } // renamed start() to mybegin()
7 myiterator myend() { return mybegin()+5; } // renamed stop() to myend()
8
9 int main()
10 {
11 using namespace std;
12
13 for (myiterator i=mybegin(); i != myend(); ++i)
14 cout << *i << ' ';
15 cout << '\n';
16 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-5.cxx
1 #include <iostream>
2
3 using myiterator = int*;
4 myiterator mybegin(int* startpos) { return startpos; } // no longer hard-coded to ar
5 myiterator myend(int* stoppos) { return stoppos+5; } // hard-coded to ar's size
6
7 int main()
8 {
9 using namespace std;
10
11 int ar[] = { 1, 2, 3, 4, 5 };
12 for (myiterator i=mybegin(ar); i != myend(ar); ++i) // pass in ar array
13 cout << *i << ' ';
14 cout << '\n';
15 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-6.cxx
1 #include <algorithm>
2 #include <iostream>
3
4 using myiterator = int*;
5 myiterator mybegin(int* startpos) { return startpos; }
6 myiterator myend(int* stoppos) { return stoppos+5; }
7
8 int main()
9 {
10 using namespace std;
11 int ar[] = { 1, 2, 3, 4, 5 };
12 for_each( mybegin(ar), myend(ar), // begin at ar[0] and process up to but not including ar[5]
13 [](int i) { cout << i << ' '; } // lambda function
14 );
15 cout << '\n';
16 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-7.cxx
1 #include <algorithm>
2 #include <iostream>
3
4 using myiterator = int*;
5 myiterator mybegin(int* startpos) { return startpos; }
6 myiterator myend(int* stoppos) { return stoppos+5; }
7
8 int main()
9 {
10 using namespace std;
11
12 int ar[] = { 2, 7, -3, 1, 4 };
13 sort(mybegin(ar), myend(ar));
14 for_each(mybegin(ar), myend(ar), [](int i) { cout << i << ' '; });
15 cout << '\n';
16 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-8.cxx
1 #include <algorithm>
2 #include <iostream>
3 #include <vector>
4
5 int main()
6 {
7 using namespace std;
8
9 vector<int> ar{ 2,7,-3,1,4 }; // replace array literal with std::vector
10 sort(begin(ar), end(ar)); // use std::begin() and std::end()
11 for_each(begin(ar), end(ar), [](int i) { cout << i << ' '; }); // likewise
12 cout << '\n';
13 }
76 / 122
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-9.cxx
1 #include <algorithm>
2 #include <execution> // For C++17 parallel algorithm's execution policies.
3 #include <vector>
4 #include <iostream>
5
6 using namespace std;
7
8 int main()
9 {
10 vector<int> ar{ 2, 7, -3, 1, 4 };
11 sort(execution::par, begin(ar), end(ar)); // parallel
12 for_each(execution::par, begin(ar), end(ar), [](int i) { cout << i << ' '; }); // parallel
13 cout << '\n';
14 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-10.cxx
1 // program is p2i2r-7.cpp modified to support ranges...
2 #include <algorithm>
3 #include <iostream>
4 #include <ranges> // For C++20 range-related items
5
6 struct mycontainer { int ar[5]; }; // a trivial "container" type to hold array
7 using myiterator = int*;
8 myiterator begin(mycontainer& c) { return c.ar; }
9 myiterator end(mycontainer& c) { return c.ar+5; }
10 static_assert(std::ranges::range<mycontainer>); // i.e., mycontainer is a (minimal) C++20 range
11
12 int main()
13 {
14 mycontainer ar{ 2, 7, -3, 1, 4 };
15 std::ranges::sort(ar);
16 std::ranges::for_each(ar, [](int i) { std::cout << i << ' '; });
17 std::cout << '\n';
18 }
Evolving Code From Pointers To Iterators To Ranges (con’t)
w02/p2i2r-11.cxx
1 #include <algorithm>
2 #include <iostream>
3 #include <ranges>
4 #include <vector>
5
6 int main()
7 {
8 using namespace std;
9 vector ar{ 2, 7, -3, 1, 4 };
10 ranges::sort(ar);
11 ranges::for_each(ar, [](int i) { cout << i << ' '; });
12 cout << '\n';
13 }
Outline
8
Overview
: Overview
In the C programming language all declarations are values: even pointers are
values.
NOTE: C++ always uses value semantics unless the type involved is
actually a reference.
: Overview (con’t)
9 Values
Values
In C++:
• unless & or && appears in a type, the type is a value
• The appearance of & or && implies the type is a reference, so its absence implies a
value.
• Due to C ++ grammar rules, & or && will typically appear as the rightmost part of a
type, e.g.,
• int&
• int&&
• std::string const&
• std::list<int>&&
• Exception: int(&)[100], i.e., reference to array
• Exception: int(&&)[100], i.e., reference to array
• Exception: int(&)(float), i.e., reference to function
• Exception: void(&&)(char), i.e., reference to function
Values (con’t)
10 Pointers
Pointers
Pointers are explicitly manipulated address values and are used and behave
as they do in C.
The invalid memory address is referred to as the null pointer.
Pointers (con’t)
There were issues with C++98’s use of 0 to represent the null pointer with some
overloading needs so C++11 fixed this by:
• defining a new keyword nullptr to represent the null pointer value
• defining a new type std::nullptr_t as the type of the value nullptr
• allowing specific null pointer values to be obtained by casting, often done
implicitly, nullptr to the desired pointer type
• e.g., int *p = nullptr;
Pointers (con’t)
Recall from C:
Pointer Declaration Interpretation
T* read-write address referring to a read-write value of type T
T const* read-write address referring to a read-only value of type T
T * const read-only address referring to a read-write value of type T
T const * const read-only address referring to a read-only value of type T
Outline
11 References
References
IMPORTANT!
References are not pointers!
References (con’t)
C++ reference variables have the same properties and semantics as what they refer to.
References (con’t)
In C++object-oriented code:
• this is always a constant pointer to the object
• this is not a reference since the introduction of this in early C++ predated the
introduction of references to the language —so this was defined to be a constant
pointer instead.
Outline
[1, [basic.lval]]
C ++ Expression Category Taxonomy (con’t)
• an lvalue:
• designates a function or an object
• is historically a value that could appear on the left-hand side of an assignment
expression
• an xvalue is an expiring object value
• i.e., it refers to an object usually near the end of its lifetime
• a prvalue is an rvalue that is not an xvalue
• a glvalue is a generalized lvalue, i.e., it designates an lvalue or an xvalue
• an rvalue:
• is an xvalue, a temporary object or a subobject of such, or a
value not associated with an object
• is historically a value that could appear on the right-hand side of an assignment
expression
[1, [basic.lval]]
Outline
13 Lvalue References
Lvalue References
14 Rvalue References
Rvalue References
In practice one can think of an rvalue as a value that does not have a
name:
• int&& i=3; // 3 is an rvalue; i is an lvalue
• int&& j=int(2); // int(2) is an rvalue; j is an lvalue
Consider a math_vector that uses dynamic memory allocation to store its values:
w02/move-eg-1.cxx
1 // Given...
2 math_vector a{1.0, 1.1, 1.2};
3 math_vector b{2.0, 2.1, 2.2};
4 math_vector c{3.0, 3.1, 3.2};
5 math_vector d{4.0, 4.1, 4.2};
6
7 // The expression...
8 auto e = a + b + c + d;
Consider an Example (con’t)
In most OOP languages, e=a+b+c+d; executes inefficiently, i.e., something like this:
• a + b returns temp1 (copy local variable on return, destroy local variable)
• temp1 + c returns temp2 (copy local variable on return, destroy local variable)
• temp2 + d returns temp3 (copy local variable on return, destroy local variable)
• temp3 becomes e
e.g., if math_vector uses dynamic memory, then RAM will be allocated, copied, and
deallocated many times inefficiently.
Consider an Example (con’t)
In C++, assuming math_vector is move aware with the code and compiler not
exploiting various optimizations, RVO, constexpr, perfect forwarding, and other
coding techniques such as expression templates, etc., then the result of
e=a+b+c+d; is much better:
• a + b returns temp1 (move local variable on return, destroy local variable)
• temp1 + c returns temp2 (move local variable on return, destroy local variable)
• temp2 + d returns temp3 (move local variable on return, destroy local variable)
• temp3 becomes e
e.g., if math_vector uses dynamic memory, then RAM will be “moved” by copying a
pointer. This is way more efficient.
Outline
There is a finite list of template patterns where the C++ compiler will do full type deduction. FYI, these
patterns are:
• T, T cv-qual, T*, T&, T&&, T[integer-constant], template-name<T>, type(T), T(), T(T), T type::*, type
T::*, T T::*, T (type::*)(), type (T::*)(), type (type::*)(T), type (T::*)(T),
T (type::*)(T), T (T::*)(), T (T::*)(T), type[i], template-name<i>, TT<T>, TT<i>, TT<>
• where:
• T is a template type argument; TT is a template template argument; i is a
template non-type argument; cv-qual are cv-qualifiers; integer-
constant is an integer constant; template-name is a class
template; type is a type;
• (T) is a parameter-type-list where at least one parameter contains a T
• () is a parameter-type-list where no parameter type contains a T
• <T> is a template argument list where at least one argument contains a T
• <i> is a template argument list where at least one argument contains an i
• <> is a template argument list where no argument contains a T or an i
Outline
std::move(value)
• Casts value into an rvalue.
• Is used by the programmer to force an lvalue to be an rvalue.
• Given: int&& r=13;
• Since r is an lvalue (it has a name!), to pass it to a function, foo(),
requiring an rvalue, one would write: foo(std::move(r));.
[1, [forward]].
std::move and std::forward (con’t)
std::forward<Type>(value)
• Used with templates to enable perfect-forwarding of function
arguments.
• Essentially, std::forward ensures value remains an rvalue if it is, otherwise, it
remains as an lvalue.
[1, [forward]].
Outline
Which implies:
1 f must be able to forward a1, a2, ..., an to E unmodified, and,
2 f must be able to return the result of E(a1, a2, ..., an) back to the
caller
unmodified.
Q. How do you write code to exploit copy and move semantics? Answers:
1 Write appropriate copy and move constructors and assignment operators for all
structs, classes, and unions.
2 Write code in a straight-forward manner:
• Avoid direct pointer manipulation by encapsulating pointers within classes.
• Prefer references over pointers.
• Prefer values over references.
• Prefer returning by value.
• Use pass-by-value over pass-by-const-reference if a function must always
modify a local copy of that argument —otherwise use pass-by-
const-reference.
• Use const if a variable/argument will never be modified.
• Optimize later only if necessary, e.g., explicitly using std::move() or
std::forward<T>().
References