What is an object in C++?
An object is a region of memory associated with a type
Unlike some other languages (Java), basic types such as int and bool are objects
For the most part, C++ objects are designed to be intuitive to use
What special things can we do with objects
Create
Destroy
Copy
Move
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
// Call constructor.
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
// Doesn't do anything
auto& vec_ref = vec_long;
// Calls copy constructor.
auto vec_short2 = vec_short;
// Calls copy assignment.
vec_short2 = vec_long;
// Calls move constructor.
auto vec_long2 = std::move(vec_long);
// Calls move assignment
vec_long2 = std::move(vec_short);
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
// Call constructor.
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
// Doesn't do anything
auto& vec_ref = vec_long;
// Calls copy constructor.
auto vec_short2 = vec_short;
// Calls copy assignment.
vec_short2 = vec_long;
// Calls move constructor.
auto vec_long2 = std::move(vec_long);
// Calls move assignment
vec_long2 = std::move(vec_short);
my_vec::~my_vec() {
delete[] data_;
}
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_short2 = vec_short;
my_vec::my_vec(my_vec const& orig): data_{new int[orig.size_]},
size_{orig.size_},
capacity_{orig.size_} {
// Should work if we also define .begin() and .end(), an exercise for the reader.
ranges::copy(orig, data_);
}
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
vec_long = vec_short;
my_vec& my_vec::operator=(my_vec const& orig) {
return my_vec(orig).swap(*this);
}
my_vec& my_vec::swap(my_vec& other) {
ranges::swap(data_, other.data_);
ranges::swap(size_, other.size_);
ranges::swap(capacity_, other.capacity_);
}
// Alternate implementation, may not be as performant.
my_vec& my_vec::operator=(my_vec const& orig) {
my_vec copy = orig;
std::swap(copy, *this);
return *this;
}
void f(my_vec&& x);
void inner(std::string&& value) {
value[0] = 'H';
std::cout << value << '\n';
}
void outer(std::string&& value) {
inner(value); // This fails? Why?
std::cout << value << '\n';
}
int main() {
f1("hello"); // This works fine.
auto s = std::string("hello");
f2(s); // This fails because i is an lvalue.
}
// Looks something like this.
T&& move(T& value) {
return static_cast<T&&>(value);
}
void inner(std::string&& value) {
value[0] = 'H';
std::cout << value << '\n';
}
void outer(std::string&& value) {
inner(std::move(value));
// Value is now in a valid but unspecified state.
// Although this isn't a compiler error, this is bad code.
// Don't access variables that were moved from, except to reconstruct them.
std::cout << value << '\n';
}
int main() {
f1("hello"); // This works fine.
auto s = std::string("hello");
f2(s); // This fails because i is an lvalue.
}
class T {
T(T&&) noexcept;
T& operator=(T&&) noexcept;
};
class my_vec {
// Constructor
my_vec(int size)
: data_{new int[size]}
, size_{size}
, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_short2 = std::move(vec_short);
my_vec::my_vec(my_vec&& orig) noexcept
: data_{std::exchange(orig.data_, nullptr)}
, size_{std::exchange(orig.size_, 0)}
, capacity_{std::exchange(orig.size_, 0)} {}
class my_vec {
// Constructor
my_vec(int size): data_{new int[size]}, size_{size}, capacity_{size} {}
// Copy constructor
my_vec(my_vec const&) = default;
// Copy assignment
my_vec& operator=(my_vec const&) = default;
// Move constructor
my_vec(my_vec&&) noexcept = default;
// Move assignment
my_vec& operator=(my_vec&&) noexcept = default;
// Destructor
~my_vec() = default;
int* data_;
int size_;
int capacity_;
}
auto vec_short = my_vec(2);
auto vec_long = my_vec(9);
vec_long = std::move(vec_short);
my_vec& my_vec::operator=(my_vec&& orig) noexcept {
// The easiest way to write a move assignment is generally to do
// memberwise swaps, then clean up the orig object.
// Doing so may mean some redundant code, but it means you don't
// need to deal with mixed state between objects.
ranges::swap(data_, orig.data_);
ranges::swap(size_, orig.size_);
ranges::swap(capacity_, orig.capacity_);
// The following line may or may not be nessecary, depending on
// if you decide to add additional constraints to your moved-from object.
orig.clear();
return *this;
}
void my_vec::clear() noexcept {
delete[] data_
data_ = nullptr;
size_ = 0;
capacity = 0;
}
struct S {
// modernize-pass-by-value error here
S(std::string const& x) : x_{x} {}
std::string x_;
};
auto str = std::string("hello world");
auto a = S(str);
auto b = S(std::move(str));
Consider the following code
struct S {
// modernize-pass-by-value error no longer here
S(std::string x) : x_{std::move(x)} {}
std::string x_;
};
auto str = std::string("hello world");
auto a = S(str);
auto b = S(std::move(str));
Now consider the following
class T {
T(const T&) = delete;
T(T&&) = delete;
T& operator=(const T&) = delete;
T& operator=(T&&) = delete;
};
A concept where we encapsulate resources inside objects
eg. Memory, locks, files
Every resource should be owned by either:
Another resource (eg. smart pointer, data member)
The stack
A nameless temporary variable
To create safe object lifetimes in C++, we always attach the lifetime of one object to that of something else
auto okay(int& i) -> int& {
return i;
}
auto okay(int& i) -> int const& {
return i;
}
auto questionable(int const& x) -> int const& {
return i;
}
auto not_okay(int i) -> int& {
return i;
}
auto not_okay() -> int& {
auto i = 0;
return i;
}