10B_OperatorOverloading
10B_OperatorOverloading
0:0:0 3:57:19
4:2:56
0:2:4
Motivation
8:0:15
8:0:14
2:0:4
Motivation
// compare
std::cout << ( (t2 < t1) ? "t1 longer" : "t1 not longer" ) << '\n';
t1 not longer
3
7204
Operator Overloading: the Gist
The next operator evaluation is already written into <iostream> for how
std::cout combines on the left of << with a
std::ios_base& (*)(std::ios_base&) object, such as '\n'.
Historical Aside on << and >>
The meaning of << and >> outside of our convenient overloads has to do
with bit shifting. Recall that integer types are stored as a collection of
bits (on/off, 1/0) and these bit patterns are interpreted as numbers in
binary (base 2).
We decompose a number into sums of powers of the base. This content is protected
and may not be shared, uploaded, or distributed.
PIC 10B, UCLA
Useful digression: there are binary and unary versions of + and -. The
binary versions add/subtract two arguments. The unary + and - are used
below for ints:
int x = 9;
std::cout <<+x; // prints 9: unary +
std::cout <<-x; // prints -9: unary -
Operator Overloading: the Gist
Style conventions:
Operators like += are "asymmetric" in the sense that usually just one of
the two parameters is modified (the one on the left).
Operator Overloading: the Gist
and the comparison operators <, >, ==, !=, <=, >=, etc.
Also, some of these operators operate from left to right, like +, and others
from right to left, like =.
Operator Overloading: the Gist
We can also only overload operators for classes and only those operators
that do not already have a meaning. We can’t change the meaning of *
for ints, for example.
For brevity, the operators/constructors are not documented with /** */ but
for practical code, the operators should all be documented.
Time Class Setup II
class Time {
private:
int hours, minutes, seconds; // data members
public:
// accepts number of seconds, uses 0 for a default construction
constexpr Time(int _seconds = 0);
// sets the time from input hours, minutes, and seconds given
constexpr Time(int _hours, int _minutes, int _seconds) :
Time(sec_per_hour * _hours + sec_per_min * _minutes + _seconds)
{}
// unary -
constexpr Time operator-() const
{ return Time(-hours,-minutes,-seconds); }
};
Time Class Setup VI
// to compare
constexpr bool operator<(const Time& left, const Time& right) {
return left.operator int() < right.operator int();
}
// user-defined literal
constexpr Time operator ""_sec(unsigned long long int seconds);
Time Class Implementations: private
private:
int hours, minutes, seconds; // data members
The minutes and seconds will range from -59 to 59. The hours can be
any int.
The private member functions are for use by the class itself.
The reduce member function should find the total number of signed
(could be negative) seconds found in the number of hours, minutes, and
seconds stored in the object; then, it should compute the corresponding
number of hours, minutes, and seconds that correspond to that number
of seconds.
If the total number of seconds should be negative, we will have that all of
the hours, minutes, and seconds are nonpositive: -63 seconds should be
-1 minute and -3 seconds.
Time Class Implementations: constexpr and inline
And many member functions can also be constexpr when they only
involve fundamental types and reading/setting values that can be done at
compile time.
Time Class Implementations: constexpr and inline
This means the definition of such a function should appear in the header
file in which it is declared! We can still separate the declaration and
definition but they must both be in the header file.
}
Time Class Implementations: Default Constructor
The Time class is a literal type. As such, it is often possible that we can
construct a Time object as a constant expression and we are allowed
to mark some of its constructors as consexpr. Such constructors can be
run at compile time.
The precise definition of a literal type from the C++ Standard Follows but
in a nutshell for our purposes, an entity is a literal type if it only stores
fundamental types, other literal types, and its destructor is not virtual (so
no base classes with virtual destructors either!) and the destructor does
not need to free any memory/resources.
Time Class Implementation: Default Constructor
A type is a literal type if it is:
— possibly cv-qualified void; or
— a scalar type; or
— a reference type; or
— an array of literal type; or
— a possibly cv-qualified class type [...] that has all of the following
properties:
— it has a trivial destructor,
— it [...] has at least one constexpr constructor [...]
— if it is a union, at least one of its non-static data members is of
non-volatile literal type, and
— if it is not a union, all of its non-static data members and base classes
are of non-volatile literal types.
[ Note: A literal type is one for which it might be possible to create an
object within a constant expression. It is not a guarantee that it is
possible to create such an object, nor is it a guarantee that any object of
that type will be usable in a constant expression. — end note ]
Time Class Implementations: Default Constructor
We define the constructor below (note how the default argument is not
repeated in the definition):
3922/3600 = 1 and
(3922 % 3600) = 322 and 322/60 = 5 and
3922 % 60 = 22.
Time Class Implementations: Default Constructor
Further assignments are permitted in the body, but all members must be
initialized through a constructor initializer list. In general, a constexpr
constructor must satisfy the conditions of an ordinary constexpr function.
Time Class Implementations: Time(int, int, int)
reduce();
}
Imagine reading from a file times.txt line by line to get Times but the file
has a format like:
3 14 0
9 22 53
0 something very evil
2 4 43
The exception, if managed, would prevent that evil line from corrupting
the Times.
Time Class Implementations: Time(std::istream&)
Not entirely necessary here given we check the state of the stream, if we
saw a bunch of gigantic int-values floating around in the times, this would
suggest a reading error.
Time Class Implementations: Time(std::istream&)
Time t(10,11,12);
would generate
-10:-11:-12
Time Class Implementations: operator+=
We define operator+= outside the class:
reduce(); // format
outputs
0:0:11 0:0:9
It is resolved as:
t1 now (0,0,2), returned by reference
z }| {
t1 += t2 += t1+=t1 ;
t2 now (0,0,9), returned by reference
z }| {
t1 += t2 +=t1 ;
t1 now (0,0,11), returned by reference
z }| {
t1 += t2 ;
t1;
Right to Left
While a similar output could be achieved in returning by value instead of
reference, there would be an additional overhead of creating temporary
objects (and destroying them), whereas the references ensure there are
no copies being made.
All of
+= -= *= /= %= =
are right-to-left operators.
Consider:
Time t1(0,0,1), t2(0,0,7), t3;
t3 = t2 = t1; // now all are 0:0:1
It is resolved as:
t2 now (0,0,1), returned by reference
z }| {
t3 = t2=t1 ;
t3 now (0,0,1), returned by reference
z }| {
t3 =t2 ;
t3;
Time Class Implementations: operator- -
There are prefix and postfix versions of ++ and - -; the way the compiler
tells them apart is by requring the postfix to accept an unused int
parameter. Otherwise they would have the same signature.
The prefix versions are unary; the postfix versions are binary.
Time Class Implementations: operator- -
With the postfix, we follow the convention that the object is copied first;
then, it gets decremented; finally, the copy of the original,
pre-decremented object is returned.
Because we never use unused, we can even remove its name altogether.
The only way the compiler can find the definition of the postfix ++ or - - is
by finding an operator++ or opeator- - that explicitly accepts an int!
Time Class Implementations: operator- -
Assuming prefix++:
Time t(0,0,58);
++++++t; // now t == Time(0,1,1)
int x = 7;
++++x; // now x == 9
double y=4.2;
std::cout <<y- -; now y==3.2 and the output was 4.2
The name of the operator above is operator(), and because this version
takes no arguments, there is an extra set of parentheses indicating no
arguments are taken. Had there been arguments to the operator, they
would appear in a comma separated list in the second pair of
parentheses.
Time Class Implementations: operator()
return *this;
}
Time Class Implementations: operator()
Time t(1,2,3);
t(); // now t == Time(0,0,0)
Time Class Implementations: operator[]
We consider the subscript operator with valid subscripts ‘h’, ‘m’, and ‘s’
for the hours, minutes, and seconds. Anything else is invalid.
v[0] = 7;
For the Time class, giving outside access to the private variables of
hours, minutes, and seconds could be disastrous because if a user
directly modified those without the class object being reduced, the
outputs could become garbled. Thus we chose to just return the member
variables by value.
Time Class Implementations: operator<<
Note that we use and call upon the local reference variable out here
(could be std::cout but could be an output file stream or output string
stream): we aren’t just using std::cout - gotta thank Polymorphism for
that!
Time Class Implementations: Conversion Operator
If this operator had been defined outside the class, it would be defined as:
Remark: the return type does not appear before the function name
because the function name operator int() const already says what that
is, int in this case.
Time Class Implementations: Conversion Operator
Right now, the operator allows for implicit conversions. For example:
std::cout << t;
3600
To prevent the implicit conversions and only allow this for static_casts,
we use the explicit keyword as above.
The explicit keyword only appears within the class interface and not
without.
With explicit:
constexpr Time t;
int j = t; // ERROR
int j = static_cast<int>(t); // okay, j could be constexpr, too
This content is protected and may not be shared, uploaded, or distributed.
PIC 10B, UCLA
The friend keyword also makes it explicit that this function is not a
member function of the class.
Time Class Implementations: operator>>
The friend keyword should be used sparingly: with it, a class willingly
violates encapsulation granting another function/class access to its
private/protected members.
Time Class Implementations: operator>>
Since we define it inside the class, to prevent the compiler from trying to
make it into a member function (which would then require 3 arguments:
the implicit object plus the two listed - nonsense for a binary operator), it
knows the function is not a member because of the friend keyword.
Time Class Implementations: operator>>
A friend function can be defined inside or outside the class. When
declared inside, it must have the friend keyword but it cannot have the
friend keyword outside of the class.
t.reduce();
return in;
}
Time Class Implementations: operator<
Recall that left will be updated and its value will be returned by reference
from +=; this reference will then be used to copy initialize the return value.
Note: a copy is made of the first variable! Despite Time being an object,
we passed by value! If we passed it as a reference to const, we’d need to
make a copy if it inside anyway because the binary addition isn’t
supposed to modify its input arguments.
// LESS EFFICIENT
constexpr Time operator+(const Time& left, const Time& right) {
Time copy(left); // making a copy
return copy += right;
}
We use this operator to multiply a Time either on the left or the right by a
double.
Definitions:
Now, we can give our own suffixes converting between different types
and values.
1.609344
User Defined Literals
The conversion from miles to km could have been accomplished with the
definition:
User defined literals can be invoked by calling their name, which must
begin with an underscore _.
Not all inputs types are currently permissible; we will look at two currently
permissible numeric type inputs.
/**
Given input time in seconds and makes a Time object out of it
@param u a time in seconds as unsigned long long int
@return a Time object with that many seconds
*/
constexpr Time operator "" _sec(unsigned long long u) {
return Time(static_cast<int>(u)); // cast to int for seconds
}
and obtain
0:1:3
Time Class Implementations: operator==
With operator< and operator== defined, we can define all the other
boolean operators.
0:0:3
0:2:0
1:0:0
Operators Are Just Functions
It’s important to remember that operators are just functions. If t1 and t2
are Time variables, then the following are equivalent:
namespace A {
struct Foo { };
constexpr Foo operator+(Foo a, Foo b) { return { }; };
namespace literals { // nested namespace
constexpr Foo operator""_foo(unsigned long long u) { return { }; }
}
}
// ...
using namespace A::literals;
3_foo + A::Foo{}; // okay, calls A::operator+(A::Foo, A::Foo)
Declaration vs Definition
Since the operator+= is declared within the interface and the operator+
appears after that interface, we are safe.
Declaration vs Definition
As long as the compiler knows what everything is, it can suitably put
placeholders for items that are undefined; then, the linker needs to fill in
the details by finding the definitions.
Declaration vs Definition
The C++ Standard has some delicate rules in linking h- and cpp-files
called the One Definition Rule.
"No translation unit shall contain more than one definition of any variable,
function, class type, enumeration type, or template."
“There can be more than one definition of a ... class type ..., enumeration
type ..., class template ..., non-static function template ..., concept ...,
static data member of a class template ..., member function of a class
template ..., default argument for a parameter (for a function in a given
scope) ..., default template argument ... in a program provided that each
definition appears in a different translation unit."
Declaration vs Definition
class X { }; // Defines the class X, which does nothing and stores nothing
struct Y : public X { // Defines the class Y
void foo() const;
};
struct X {
void f() const { } // inline because defined in class
bool g() const;
double d() const;
};
X.h
struct X { };
A.h
#include "X.h"
struct A { X x; };
B.h
#include "X.h"
struct B { X x; };
main.cpp
#include "A.h"
#include "B.h"
int main() { return 0; }
Declarations and Definitions
Translates into:
processed_main.cpp
struct X { }; // from A.h included that includes X.h
struct A { X x; }; // from A.h defining A
X.h
#ifndef _X_ B.h
#define _X_ #ifndef _B_
struct X { }; #define _B_
#endif #include "X.h"
struct B{ X x; };
A.h #endif
#ifndef _A_
#define _A_ main.cpp
#include "X.h" #include "A.h"
struct A{ X x; }; #include "B.h"
#endif int main() { return 0; }
Declarations and Definitions
A.h is included. It checks whether the symbol _A_ has been defined (it
hasn’t) so it defines that symbol and includes X.h.
From X.h it checks if the symbol _X_ has been defined (it hasn’t) so it
defines that symbol and allows struct X {}; to be dumped into the
translational unit.
B.h is then included. It checks whether the symbol _B_ has been defined
(it hasn’t) so it defines that symbol and includes X.h.
With X.h it checks if the symbol _X_ has been defined (it has!) so it does
nothing more.
Then it dumps struct B { X x;}; into the translational unit and proceeds to
define the main routine.
struct X { };
struct A { X x; };
struct B { X x; };
int main() { return 0; }
Declarations and Definitions
A variable is declared with the extern keyword without an initialization;
otherwise it is a definition. Declarations may come up in global variables.
x.cpp
int x = 42; /* x is defined here to keep linker happy when x definition
sought */
main.cpp
#include "H.h"
int main() // x global, declared in H.h, defined in x.cpp
{
return x;
}
Declarations then Definitions
Consider the program below:
#include <iostream>
struct X {
void f() const { g(); } // ERROR: what is g()?
Y *yp; // ERROR: what is a Y?
};
int main() {
X().f();
return 0;
}
Declarations then Definitions
The member function f() of X calls upon g(). Unfortunately, the compiler
reads code sequentially and it does not know what g() is! This can be
fixed by:
I declaring and/or defining void g() before the class
I only declaring void X::f() const and implementing void X::f() const
after the declaration and/or definition of void g()
Declarations then Definitions
The data type Y (and hence Y*) is unknown to the compiler by the time
the X interface is read. This can be fixed by:
I declaring Y before the class with a simple struct Y; or
I defining Y above the definition of X.
Declarations then Definitions
The only functions that can be defined more than once in a program are
the inline functions, and they then must be defined once in every
translational unit where they are used.
More on Inlining and Linking
The code below fails to run as the non-inline S::foo() is defined more
than once.
H.h
#ifndef _H_
#define _H_
struct S { void foo() const; };
void S::foo() const { }
#endif
A.cpp
#include "H.h"
// definition of S::foo() const included here
Main.cpp
#include "H.h"
// definition of S::foo() const included here
int main() { S().foo(); return 0; }
More on Inlining and Linking
Note that by virtue of an inline function being defined in a header file, its
definition will appear in each translational unit which includes that header.
B.cpp
#include "H.h"
// baz is defined here but nowhere else
void baz() { bar(); } // inline bar defined here because of H.h
Main.cpp
#include "H.h"
int main () { // inline bar defined here because of H.h
bar(); baz(); return 0;
}
More on Inlining and Linking
The C++ standard states that “if the inline specifier is used in a friend
declaration, that declaration shall be a definition or the function shall have
previously been declared inline.”
Note that it is still necessary the definition of the inline function appear in
each translational unit.
The best practices here would be to just declare the function a friend and
not worry about inlining (so define it in another cpp file) or to define the
function where it is declared in the class.
More on Inlining and Linking
It can fix the error, but static means something different here. It actually
tells the compiler to define a separate function (with identical set of
instructions) in every translational unit. This is not what the static
keyword was meant for.
operator,
The comma operator has the lowest precedence of all the operators.
int a = 7;
int b = 6;
a = 3, b = 4; // now a==3, b==4
This content is protected and may not be shared, uploaded, or distributed.
PIC 10B, UCLA
More examples:
int x, y, z;
A class with the call operator is callable (also called a functor). Such
classes, including lambdas (unnamed callable objects), can be used as
parameters for different generic algorithms.
If no elements are found where the function returns true, it returns the
past-the-end iterator.
But maybe we don’t want to have to write such a precise function to begin
with. Instead, we can replace the function we pass with a callable class...
Callable Classes and Lambdas
We write the class
struct inRange {
Time min, max;
Note how depending upon the input parameters to its constructor, this
class can store any min or max time.
Now we have more freedom about what time range we pick as we are not
tied to a single function definition.
The call operator is used as a predicate to check each time value in the
raceTimes vector.
This gives us more freedom in the range than with writing the
four_to_five function, but we are still restricted to consider the inclusive
range of values. If instead we wanted to see if any times were above
5:0:0 or below 4:0:0, we’d need to write another callable class or
function...
Callable Classes and Lambdas
A lambda is an unnamed callable object that can be constructed via:
class something{
private:
member variables;
public:
// CONSTRUCTOR
something(values for variables) : /* initialize them */ {}
// CALL OPERATOR
call return type operator() (call operator parameters) const
{ call body }
};
Callable Classes and Lambdas
The capture list and call operator arguments should be comma separated
but can also be empty.
The capture values are variables in the scope where the lambda is
defined that it needs to use. Think of these as constructor arguments.
The call parameters are the parameters the lambda requires for its call
operator: a lambda is a callable class, so we need to be able to call it!
The return type follows the arrow; on some compilers it is not required if
the body is short enough but for robustness, it should be included!
Time lower(4,0,0);
Time upper(5,0,0);
The lambda needs access to lower and upper for comparison: they are
unknown within its body unless they are captured!
We can only store the lambda with the auto keyword because each
lambda we create is created inline as its own unique type of class.
Remark: since C++14, the return type of a lambda can be deduced and
is not required. Sometimes it can be nice to include it for more clarity, but
it is not required and sometimes even a hindrance.
Callable Classes and Lambdas
int n = 11;
The idea is that every time a lambda is called, it should by default exhibit
the same behaviour.
Callable Classes and Lambdas
We can use the mutable keyword after the call parameters and allow for
modification of captured values. Note that the lambda will only modify its
local copy of the variable.
int n = 11;
struct Bah {
mutable int i;
The use of mutable is rare, but think, for example of the assignment
operator for std::shared_ptrs:
Because the internal reference count would change here, the assigned
from object is modified, but it can still be marked as const for all other
intents and purposes.
Callable Classes and Lambdas
int x = 1, y = 2;
auto lam = [=]()->void{ std::cout <<x + y; }; // okay
There are a number of conditions in which the compiler may or may not
generate a default constructor, copy/move constructor, copy/move
assignment operator, or destructor.
The compiler will not overrule the decision of the programmer, so any
version of the above a programmer writes is the “official” version of the
function.
What Is Compiler Generated?
These concerns are more pronounced when we consider the big five:
destrutor, copy constuctor, copy assignment operator, move constructor,
and move assignment operator.
In many classes, we don’t need to write any of them, however, and can
allow the compiler to generate them for us. As long as our individual
member variables can be copied/moved and obey RAII, there’s no need
to write them!
What Is Compiler Generated?
Compiler Provides
Default Constructor
Move Assignment
Move Constructor
Copy Assignment
Copy Constructor
Destructor
Programmer Declares
From the table, we see that, for example, if a class has a destructor
declared then move semantics are gone. But by re-enabling their default
behaviour as we did in polymorphism, the copy semantics are not
generated by default unless we re-enable them as well.
Assignment operator= (copy)
Recall we wrote a copy assignment for basic::string that involved writing
all the work for the copy constructor. Here, we get around this by letting
the copy constructor do its job...
class string {
// stuff ...
class string {
// stuff
return *this;
}
};
Assignment operator= (just one!)
Since the copy and move constructors are already written, if we are
assigning from an lvalue then that will be constructed through the copy
constructor, making it a separate independent copy.
We then swap the resources so that *this has the correct memory and
value and allow that to be destroyed as it holds the data of *this we
wanted to overwrite.
Assignment operator= (just one!)
The work we’ve done here falls under the term of the copy and swap
idiom in C++.
struct Foo {
// ...
Foo& operator=(Foo) &; // note the extra &
};
int main() {
constexpr const int *ap = &a; // okay
constexpr const int &ar = a; // okay
constexpr int i = 7;
constexpr const int *ip = &i; // ERROR: &i not constant expression here
constexpr const int &ir = i; // ERROR: needs &i
return 0;
}
Aside: Unions
union A {
int i;
double d;
};
// ...
A x;
x.i = 3; // so x.i == 3, x.d == ???
x.d = 1.7; // so x.d == 1.7, x.i == ???
Aside: Preprocessor Marcos
There are lots of functions we can do with the preprocessor alone with its
#define commands, etc. #define defines a symbol as whatever comes
next to it in a line (with some function syntax, too). Just as a few
examples:
#define ONE 1
#define TWO 2
I There are a variety of cases when the compiler will or will not
synthesize a default constructor, move/copy constructor, move/copy
assignment operator, or destructor.
I A lambda is an unnamed callable class; callable classes can be
used in generic algorithms.
I User-defined literals can act as shorthand.
I The copy and swap idiom allows for easy-to-implement copy and
move assignments by copying/moving from the temporary operator
argument.
I Ref qualifiers allow for a member function to only be invoked on
lvalues or rvalues.
Exercises I