[go: up one dir, main page]

0% found this document useful (0 votes)
3 views111 pages

Wee2 A

The document provides an overview of operator overloading in C++, detailing how various operators can be overloaded as member functions, free functions, and friend functions. It explains the rules and conventions for overloading unary and binary operators, as well as the introduction of relational operators in C++20, which simplifies the process of defining comparisons. Additionally, it includes code examples to illustrate the concepts discussed.

Uploaded by

subah468
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views111 pages

Wee2 A

The document provides an overview of operator overloading in C++, detailing how various operators can be overloaded as member functions, free functions, and friend functions. It explains the rules and conventions for overloading unary and binary operators, as well as the introduction of relational operators in C++20, which simplifies the process of defining comparisons. Additionally, it includes code examples to illustrate the concepts discussed.

Uploaded by

subah468
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 111

Advanced Object-

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

<REGEX>, <CHRONO >, <F ILE SYST EM>

DR. ABDULRAUF G IDADO WI TH EXAMP LES AND FIGURE S


F R OM ST RO UST RUP , BJ AR NE (20 23). A T OUR OF C++,
TH IR D E DITIO N. A DDIS ON -W ESL EY PR O FE SSI ON AL ,
BOSTON, MA. AND PAUL P. SLIDES
1 Outline

• 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

DR . ABDU LRAU F G ID ADO WI T H EX AM PLE S AND


F IG URES F ROM STROUSTR UP, BJ ARN E (20 2 3) . A TOUR
O F C+ +, T H I RD E DI T I ON . ADD IS O N- WE S LE Y
PRO F E SS I O NAL , BO S T O N, M A. AN D PAUL P. S LI DE S
Operator Overloading: Overview

Except for the:


• . (member access)
• .* (member access through pointer to member)
• :: (scope access)
• ?: (ternary operator), and,
• the preprocessor operators

all other operators in C++ can be overloaded.


Operator Overloading: Overview (con’t)

Some operators can only be overloaded as non-friend


member functions:
• = (assignment operator)

•() (function call operator)


•[] (array operator)
•-> (struct/class/union indirection operator)
• in C++, Operator overloading is a compile-time polymorphism.
• C++ has the ability to provide the operators with a special meaning for a data
type, this ability is known as operator overloading

• int number1;
float number2, sum;
sum = number1 + number2;

• Class coding examples


Operator Overloading: Overview (con’t)

Overloading an operator must involve at least one user-defined type:


operator overloading cannot redefine any language-defined operator
definitions.
New operators cannot be created.
It is not possible to change the precedence, grouping, or the number of
operands of an operator.
Any operators that have short-circuiting or sequencing features, e.g., &&,
||, and ,, don’t exhibit such when user-defined overloads are called.
Operator Overloading: Overview (con’t)

Any overload of operator->() must return a raw pointer


or an object that overloads
operator->().
Avoid overloading operators unless such is necessary. When
overloading operators, preserve the conventional meaning of
such to avoid confusing other programmers (and yourself) as
to what that operator does when used with a specific type.
Operator Overloading: Unary, As Free Function

An example of overloading the unary + operator as a free function:


w02/op-overload-free.cxx
1 struct foo { int i; };
2
3 auto operator+(foo const& a)
4 {
5 return foo{ a.i+3 };
6 }
Operator Overloading: Unary, As Member Function

An example of overloading the unary + operator as a member function:


w02/op-overload-class.cxx
1 struct foo
2 {
3 int i;
4
5 auto operator+() const // notice there is no argument
6 // the object itself is the first argument
7 {
8 return foo{ this->i+3 };
9 }
10 };
Operator Overloading: Unary, As Member Function (con’t)

An example of overloading the unary + operator as a friend function defined when it


is declared as a friend:
w02/op-overload-friend.cxx
1 struct foo
2 {
3 int i;
4
5 friend auto operator+(foo const& a)
6 {
7 return foo{ a.i+3 };
8 }
9 };

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)

An example of overloading the unary + operator as a friend function (using a


prototype and the function is defined elsewhere):
w02/op-overload-friend2.cxx
1 struct foo; // forward declaration
2 auto operator+(foo const&); // function prototype
3
4 struct foo
5 {
6 int i;
7 friend auto operator+(foo const&); // friend declaration
8 };
9
10 auto operator+(foo const& a) // definition
11 {
12 return foo{ a.i+3 };
13 }
Operator Overloading: Binary, As Free Function

An example of overloading the binary + operator as a free function:


w02/op-overload-free-binary.cxx
1 struct foo { int i; };
2
3 auto operator+(foo const& a, foo const& b)
4 {
5 return foo{ a.i + b.i };
6 }
Operator Overloading: Binary, As Member Function

An example of overloading the binary + operator as a member function:


w02/op-overload-class-binary.cxx
1 struct foo
2 {
3 int i;
4
5 auto operator+(foo const& rhs) const // argument is the second operand
6 // this object is the first operand
7 {
8 return foo{ this->i + rhs.i };
9 }
10 };
Operator Overloading: Binary, As Member Function (con’t)

An example of overloading the binary + operator as a friend:


w02/op-overload-friend-binary.cxx
1 struct foo
2 {
3 int i;
4
5 friend auto operator+(foo const& a, foo const& b)
6 {
7 return foo{ a.i + b.i };
8 }
9 };
Operator Overloading: Prefix and Postfix + and - Operators

The postfix operators, ++ and --, are special:


• the postfix operator has an additional unused int
parameter that the compiler will internally optimize away.

This allows the function definitions of the two variants of each


postfix operator to be distinguished.
Operator Overloading: Prefix and Postfix + and - Operators (con’t)

By convention, prefix operator overloads should return lvalues.


By convention, postfix operator overloads should return rvalues.
Operator Overloading: Prefix and Postfix + and - Operators
(con’t)
Often the postfix operator overload can be defined in terms of the prefix overload:
w02/op-overload-prepostfix.cxx
1 struct foo
2 {
3 int i;
4 foo& operator++()
5 {
6 ++this->i;
7 return *this;
8 }
9
10 foo operator++(int) // postfix operator
11 {
12 foo const retval{*this}; // make a copy
13 this->operator++(); // call prefix operator on *this
14 return retval; // return the copy
15 }
16
17 };
Operator Overloading: IOStream Operator Overloads
The IOStreams hierarchy makes use of operator overloads to produce formatted
output, e.g.,
w02/op-overload-io.cxx
1 #include <iostream>
2
3 struct foo { int i; };
4
5 std::ostream& operator<<(std::ostream& os, foo const& f) // i.e., notice const&
6 {
7 os << f.i;
8 return os;
9 }
10
11 std::istream& operator>>(std::istream& is, foo& f) // i.e., notice & and no const
12 {
13 is >> f.i;
14 return is;
15 }
16
Operator Overloading: IOStream Operator Overloads (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)

• defining an = defaulted operator<=> function will automatically generate all of


the relational operators
• unless the user explicitly wrote / deleted specific relational operator definitions
• i.e., the compiler will favour using user-defined definitions / deletions over
generating definitions
• either or both operator== and operator<=> can be = defaulted
• when done, the compiler generates its definitions processing each data member in
the order of appearance in the type definition, i.e., using so-called lexicographical
ordering (of data members)
• if operator<=> has an auto return type, then the compiler will determine the
strongest ordering possible based on the ordering of each of the data members in
order to select one of std::strong_ordering, std::weak_ordering, or
std::partial_ordering
Relational Operators: C++20 (con’t)
• if there is no operator<=> then there will be no auto-generation of operator<,
operator<=, operator>=, operator>
• when using operator<=> one must include the <compare> header file
• sometimes it is possible that automatically-generated code will be inefficient
• e.g., this can happen if there is a more efficient algorithm than lexicographically
computing operator==
• e.g., if you were implementing a string class that stores the string and the size of the
string, you would write operator== to first compare the sizes of the two strings
before comparing each element of the strings as that is more efficient; —the
compiler-generated version (if it was even correct —it might not be if pointers are
involved) would be lexicographical and therefore dependent on the order data
members were declared possibly causing all elements to be compared before the sizes
of the two strings.
• Solution: Write an explicit definition of operator== (and any other operator) when
there is a more efficient definition that should be used instead.
Relational Operators: C++20 (con’t)

• if the compiler cannot automatically-generate definitions, then compile-time errors


will result and one will have to address such or write such definitions manually
• Ensure each of these overloads’ two parameters are passed as const&.
• Ensure each relational operator returns bool.
• Ensure <=> returns one of the three orderings.
• It is possible to explicitly tell the compiler which ordering to use with
operator<=> by explicitly setting the return type of such to one of the three
orderings.
• While automatically generated code is nice, if there is a more efficient way to
implement an operator than lexicographically, implement that more efficient
operator.
• In general when this is done, operator<=> will not be able to have an auto return
type —explicitly define the ordering instead.
Relational Operators: C++20 (con’t)
So, yes, while this looks like it can be easy to use, the specifics can become quite
complicated. The ISO C++ committee put a lot of work into this in order to get the
details correct.
Usually one would define operator== (explicitly or defaulted) and operator<=>
(defaulted).
If a class’ use of all data members are as values, those data members all have
relational operators already properly defined on them, and, all of those data members
participate in determining the computed results of the relational operators, then one
can easily have the compiler generate relational operator and/or three-way-operator
code.
Otherwise one will need to write some definitions.
This is an example where the operator<=> function is explicitly written —not merely
= defaulted, e.g.
Relational Operators: C++20 (con’t)
w02/rel-since-cxx20b.cxx
1 #include <compare>
2 #include <iostream>
3 struct foo
4 {
5 int i;
6 // operator== must be written since <=> is not =default...
7 friend bool operator==(foo const& lhs, foo const& rhs) = default;
8 friend constexpr auto operator<=>(foo const& lhs, foo const& rhs) noexcept
9 {
10 #if 1
11 return lhs.i <=> rhs.i; // use built-in operator<=>(int,int) definition
12 #else // or do this more manually...
13 if (lhs.i < rhs.i)
14 return std::strong_ordering::less;
15 else if (lhs.i == rhs.i)
16 return std::strong_ordering::equal;
17 else
18 return std::strong_ordering::greater;
19 #endif
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

In mathematics, a predicate is a function that returns a boolean value.


In C ++ predicates return bool and are either unary or binary. They are used
extensively in the Standard Library.
C++11 introduced standard syntax for implementation-defined language extensions to
the C++ language called attributes:
• can be used almost everywhere in a C++ program
• can be applied to almost everything
• only valid where the implementation permits it to be valid
• implementations may ignore unknown attributes without causing an error
• normally apply to the preceding entity
• With declarations, attributes may appear before or after the name.
• all non-standard attributes are in an attribute namespace for that
implementation, e.g., [[gnu::packed]], [[clang::trivial_abi]],
[[msvc::intrinsic]]
Outline

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]]

This attribute informs the compiler that a label/statment is likely or unlikely to


occur, e.g.,
w02/likely-unlikely1.cxx
1 int foo(int i)
2 {
switch (i)
3
{
4
case 1:
5
6 [[fallthrough]];
7
case 2: [[likely]]
8
return 1;
9
}
10
return 0;
11
12 }
Standard Attributes: [[likely]] and [[unlikely]] (con’t)

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 suppresses compiler warnings on an unused entity, e.g.,


w02/maybe_unused.cxx
1 int main()
2 {
3 [[maybe_unused]] int i;
4 }
Standard Attributes: [ [ n o d i s c a r d ] ]
This attribute asks the compiler to issue a warning if the return value is
discarded. There are two forms:
• [[nodiscard]]
• [[nodiscard("some text")]]
code/w02/nodiscard.cxx
1 #include <iostream>
2 struct [[nodiscard]] super_important { };
3 struct foo {
4 int i;
5 [[nodiscard]] bool operator==(foo const& f) const { return i == f.i; }
6 static super_important bar() { return {}; }
7 };
8 int main() {
9 foo a{1}, b{2};
10 a == b; // warning issued since bool is discarded
11 foo::bar(); // warning issued since super_important is discarded
12 }
Standard Attributes: [ [ n o d i s c a r d ] ] (con’t)

A number of C++ functions are declared with the [[nodiscard]] attribute


including:
• operator new and operator new[]
• various allocate() member functions
• std::launder()
• std::assume_aligned()
• various empty() member functions
• std::async()
Standard Attributes: [ [ n o r e t u r n ] ]

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)

A number of C ++ functions are declared with the [[noreturn]] attribute including:


• std::abort()
• std::exit()
• std::quick_exit()
• std::terminate()
• std::rethrow_exception()
• std::rehtrow_nested()
• std::throw_with_nested()
• std::longjmp()
Standard Attributes: [ [ n o _u n i q u e_ ad d re s s ] ]
This attribute indicates that a non-static data member need not have an
address distinct from all other non-static data members in the same class.
This allows such members to be overlapping with other non-static data
members or base class subobjects. For example, an empty class data member
can be optimized to use no space. e.g.,
code/w02/no_unique_address.cxx
1 struct foo { }; // i.e., foo is an empty class
2 struct bar1
3 {
4 int i;
5 [[no_unique_address]] foo f;
6 int j;
7 };
8 struct bar2 { int i; int j; };
9
10 static_assert(sizeof(foo) >= 1); // the size of any object must at least be one
11 static_assert(sizeof(bar1) == sizeof(bar2));
Iterators in C++ are based on the syntax and rules of pointers.

Thus, let’s review some important particulars about pointers.


Outline

6 A Review Of Pointers
A Review Of Pointers

Pointers are permitted:


• to point to a valid object,
• e.g., int ar[5]; int *p=&ar[4]; int *q=ar+3;
• e.g., int i=4; int *p=&i;
• be null (i.e., invalid), or,
• e.g., int *p=0; int *q=nullptr;
• The integer 0 is cast to the null pointer in pointer contexts.
• With C++11 or newer use nullptr instead of 0.
• point to one “position” past the valid object — as long as one never accesses
the datum pointed to.
• e.g., int ar[5]; int *p=&ar[4]+1; int *q=ar+5;
• e.g., int i=4; int *p=&i+1;
A Review Of Pointers (con’t)

Pointers are not permitted:


• to point to any address before a valid object,
• e.g., given int ar[5]; then int *p=&ar[-1]; and int *q=ar-1; are
illegal
• e.g., given int i=4; then int *p=&i-1; is illegal
• to point to any address after the one-past-the-valid-object position, or,
• e.g., given int ar[5]; then int *p=ar+6; is illegal
• e.g., given int i=4; then int *p=&i+2; is illegal
• to access an address that does not point to a valid object.
• e.g., given int ar[5]; int *p=ar+5; then int i=*p; is illegal
• e.g., given int *p=nullptr; then int i=p[0]; and *p=17; are illegal
A Review Of Pointers (con’t)

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)

Consequently, these expressions are all the same:


• array[0] ≡ *(array+0) ≡ *array
• &array[0] ≡ array+0 ≡ array
• array[3] ≡ *(array+3)
• &array[3] ≡ array+3
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

7 Evolving Code From Pointers To Iterators To Ranges


Evolving Code From Pointers To Iterators To Ranges

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.

• C only supports pass/return-by-value.


• In C pass/return-by-reference is manually managed through the use of
pointers
by manually using the address-of and indirection operators.
• i.e., since in C each pointer value is pass/return-by-value and everything is
manually managed, C only supports pass/return-by-value.
: Overview (con’t)

In the Java programming language:


• value semantics are used for all built-in types
• e.g, short, int, long, double, etc.
• reference semantics are used otherwise, e.g., classes

NOTE: C++ always uses value semantics unless the type involved is
actually a reference.
: Overview (con’t)

The C++ programming language supports:


• value and pointers, i.e., as found in C
• reference semantics:
• lvalue references since C ++98
• rvalue references since C++11
• with declarations, function argument passing, return values
• with no ability to change what the reference refers to once set.
Outline

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)

• There are fundamental and compound types.


• Fundamental types include char, short, int, long, etc. [1,
[basic.fundamental]].
• Compound types include arrays, functions, pointers, references, classes,
unions, enumerations, and pointers-to-members [1, [basic.compound]].
• Every fundamental and compound type in C++ can be decorated with const
and/or volatile.
• const renders an object’s value read-only [1, [basic.type.qualifier]]
• volatile renders an object’s value as possibly asynchronously modified [1,
[basic.type.qualifier]]
• Except as mandated by the standard when using std::signal(), do not use
volatile with/for concurrency. Instead use std::atomic.
Outline

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)

In C++98, null pointers were represented:


• by the number 0 (zero) cast to the pointer type
• C’s NULL but know using such in C++ is strongly discouraged

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)

The C++ standard states:


• “[…] Lvalue references and rvalue references are distinct types. […]” [1,
[dcl.ref]]
• “It is unspecified whether or not a reference requires storage” [1, [dcl.ref]]
• This implies that the compiler does not have to use pointers to
implement references.
• Tip: Avoid explicitly finding/using a reference’s referent address as this can have
a significant impact on generated code.
• “There shall be no references to references, no arrays of references, and no
pointers to references. […] a null reference cannot exist in a well-defined
program, because the only way to create such a reference would be to bind it
to the ‘object’ obtained by dereferencing a null pointer, which causes undefined
behavior. […] a reference cannot be bound directly to a bit-field.” [1, [dcl.ref]]
References (con’t)

Internally a reference might be implemented as a read-only pointer to some value:


Pointer Declaration Reference Declaration
T* invalid —pointer not read-only
T const* invalid —pointer not read-only
T * const T&
T const * const T const&

i.e., a reference can be thought of as being internally represented as constant


pointer
to the referent which implies:
• the referent must be provided when the reference variable is declared,
and,
• the referent cannot be changed once it has been initialized.
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

12 C ++ Expression Category Taxonomy


C ++ Expression Category Taxonomy

[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

In practice one can think of an lvalue as a variable/object that has a name:


• int i=3; // i is an lvalue
• int(3); // int(3) is not an lvalue
Lvalue References (con’t)

Lvalue references must be initialized when declared:


• by an lvalue, or,
• by an rvalue iff its referent is const
• This may cause the compiler to create a temporary variable to hold the value.
• e.g., int& i = 2; // ERROR
• e.g., int const& i = 2; // OK
Lvalue References (con’t)

The following C code:


w02/lvalue-ref-1a.c
1 void foo(int * const v, int const * const k)
2 {
3 *v += *k;
4 }
5
6 int main()
7 {
8 int i=2, j=45;
9 foo(&i, &j);
10 }
Lvalue References (con’t)

can be thought of as equivalent to this C ++ code:


w02/lvalue-ref-1b.cxx
1 void foo(int& v, int const& k)
2 {
3 v += k;
4 }
5
6 int main()
7 {
8 int i=2, j=45;
9 foo(i,j);
10 }
Outline

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

Rvalue references must be initialized by an rvalue.


Why Rvalue References?

Rvalue references allow one to define move semantics for types.

• When assigning variables, calling functions, and returning locally created


variables by value unnecessary copies of variables are made.
• This is inefficient when the variable is an rvalue as it will be typically be
soon destroyed.
• It was recognized that there was no need to copy rvalues.
• Instead moving something from an old variable into a new variable
is all that needs to be done when the old variable will (soon) be
destroyed.
• Think of moving as an optimized copy operation.
Why Rvalue References? (con’t)

The complexity of moving structs/classes with data members:


• O(moving data) = o(copying data)
• Non-dynamically allocated objects typically must copy.
• Ω(moving data) = Ω(copying pointers to the data)
• Dynamically allocated objects, e.g., std::string, greatly benefit from moves.
• Ω(moving data) /= 0
• If the cost of copying or moving ever appears to be zero, then copy elison (which
includes return value optimization (RVO)) [1, [class.copy]] was performed by
the compiler instead of copying or moving.
Consider an Example

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

15 Templates, References, and Type Deduction


Templates, References, and Type Deduction
When using templates C++ has the ability to perform full type deduction with a
variety of patterns. [1, [temp.deduct.type]]
Perhaps the most commonly used pattern is T&&, e.g.,
w02/t-ref-ref.cxx
1 #include <iostream>
2
3 template <typename T>
4 void print(T&& t) // T&& here matches ALL types incl. any ref and/or cv-qualifiers!
5 {
6 std::cout << t << std::endl;
7 }
8
9 int main()
10 {
11 int i = 65;
12 print(5.5); print("Hello"); print(i); print(char('A'));
13 }
Templates, References, and Type Deduction (con’t)

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

16 std::move and std::forward


std::move and std::forward

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

17 Perfect Forwarding Definition


Perfect Forwarding Definition

The Forwarding Problem Addressed by ISO C++ [1]


For a given expression E(a1, a2, ..., an) that depends on the (generic) parameters
a1, a2, ..., an, [...][perfect forwarding makes it] possible to write a function
(object) f such that f(a1, a2, ..., an) is equivalent to E(a1, a2, ..., an).

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.

C++ implements perfect forwarding with:


• rvalue references, move semantics, std::move, std::forward, and
some adjustments to other language features.
Outline

18 Exploiting Copy and Move Semantics


Exploiting Copy and Move Semantics

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

1 ISO/IEC. Information technology – Programming languages – C++. ISO/IEC


14882-2020, IDT. Geneva, Switzerland, 2020.
2 Nicolai M. Josuttis. The C++ Standard Library. A Tutorial and Reference. 2nd
ed. Upper Sadddle River, NJ: Addison-Wesley, 2012. ISBN: 978-0-321-62321-8.

You might also like