I tried to read up on the topic of the Rule of 3/5/0 in C++, mainly from these posts:
What is The Rule of Three?
What is the copy-and-swap idiom?
Reading is good and all, but I tried to create a minimal example for myself, and I'm not sure if I did it correctly, or accidentally overlooked something.
Let's say, for a starting point, I have a class that manages a dynamically allocated C-style array, which looks like this:
Array.h
#pragma once
#include <cstddef>
#include <ostream>
class Array
{
private:
size_t _size;
int* _arr;
void checkRange(const size_t& index) const;
public:
Array(size_t size);
int getValue(size_t index) const;
void setValue(size_t index, int value);
size_t getSize() const;
//Copy semantics
Array(const Array& other) = delete;
void operator=(const Array& other) = delete;
//Move semantics
Array(Array&& other) = delete;
void operator=(Array&& other) = delete;
~Array();
};
std::ostream& operator<<(std::ostream& out, const Array& array);
Array.cpp
#include "Array.h"
#include <cstddef>
#include <stdexcept>
#include <string>
#include <ostream>
void Array::checkRange(const size_t& index) const
{
if(index > (_size - 1))
{
std::string message{
"tried to read from array at index "
+ std::to_string(index)
+ " but maximum possible index is "
+ std::to_string(_size - 1)
};
throw std::out_of_range(message);
}
}
Array::Array(size_t size)
: _size{size}, _arr{nullptr}
{
if(size < 1)
{
return;
}
_arr = new int[size]{};
}
int Array::getValue(size_t index) const
{
checkRange(index);
return _arr[index];
}
void Array::setValue(size_t index, int value)
{
checkRange(index);
_arr[index] = value;
}
size_t Array::getSize() const
{
return _size;
}
Array::~Array()
{
delete[] _arr;
_arr = nullptr;
}
std::ostream& operator<<(std::ostream& out, const Array& array)
{
size_t arraySize{array.getSize()};
for(size_t i{0}; i < arraySize; i++)
{
out << array.getValue(i) << ", ";
}
return out;
}
main.cpp
#include "Array.h"
#include <iostream>
int main()
{
Array array1(10);
array1.setValue(3, 42);
array1.setValue(7, 43);
std::cout << array1 << std::endl;
}
I wrote this class to the best of my current C++ knowledge, if there is anything wrong here, please let me know.
Copy and move semantics are explicitly hidden from this class, but now I tried to apply the concepts from the posts I read that I linked above.
I tried to incorporate the copy-and-swap idiom to move semantics as well, because the following tutorial:
Learn C++: M.3 — Move constructors and move assignment
suggests that the copy-and-swap idiom can be applied to move semantics as well.
Now, after bringing it all together, my code looks like this:
Array.h
#pragma once
#include <cstddef>
#include <ostream>
class Array
{
private:
size_t _size;
int* _arr;
void checkRange(const size_t& index) const;
public:
Array(size_t size);
int getValue(size_t index) const;
void setValue(size_t index, int value);
size_t getSize() const;
//Copy semantics
Array(const Array& other);
void operator=(Array other);
//Move semantics
Array(Array&& other);
void operator=(Array&& other);
//Needed for copy-swap idiom
friend void swap(Array& first, Array& second) noexcept;
~Array();
};
std::ostream& operator<<(std::ostream& out, const Array& array);
Array.cpp
#include "Array.h"
#include <cstddef>
#include <stdexcept>
#include <string>
#include <cstring>
#include <ostream>
#include <algorithm>
void Array::checkRange(const size_t& index) const
{
if(index > (_size - 1))
{
std::string message{
"tried to read from array at index "
+ std::to_string(index)
+ " but maximum possible index is "
+ std::to_string(_size - 1)
};
throw std::out_of_range(message);
}
}
Array::Array(size_t size)
: _size{size}, _arr{nullptr}
{
if(_size < 1)
{
return;
}
_arr = new int[size]{};
}
int Array::getValue(size_t index) const
{
checkRange(index);
return _arr[index];
}
void Array::setValue(size_t index, int value)
{
checkRange(index);
_arr[index] = value;
}
size_t Array::getSize() const
{
return _size;
}
//Copy semantics
Array::Array(const Array& other)
: _size{other._size}, _arr{nullptr}
{
if(_size < 1)
{
return;
}
//Make a deep copy of the C-style array
_arr = new int[_size]{};
std::memcpy(_arr, other._arr, sizeof(int) * _size);
}
//Applying copy-swap idiom: other object is passed by value!
void Array::operator=(Array other)
{
swap(*this, other);
}
//Move semantics
Array::Array(Array&& other)
{
//Copy and swap idiom works here too
//We don't have to deallocate our resources because after we "steal" its resources,
//the other object (which is an r-value) will do that automatically
//when it goes out of scope
swap(*this, other);
}
void Array::operator=(Array&& other)
{
//same as above
swap(*this, other);
}
Array::~Array()
{
delete[] _arr;
_arr = nullptr;
}
void swap(Array& first, Array& second) noexcept
{
std::swap(first._arr, second._arr);
std::swap(first._size, second._size);
}
std::ostream& operator<<(std::ostream& out, const Array& array)
{
size_t arraySize{array.getSize()};
for(size_t i{0}; i < arraySize; i++)
{
out << array.getValue(i) << ", ";
}
return out;
}
Did I implement the concepts of the Rule of 5 correctly? Is there anything else that could be wrong here?
Related
I have the code that confused me when I'm using operator overloading and returning the temp object it calls my copy constructor and I'm getting exception but when I'm returning my class member temp.size it calls my parameterized constructor MyClass(int size) and everything works fine. I'm interested in how it works and what it's related to. Code below.
class MyClass
{
private:
int* data;
int size;
public:
MyClass()
{
size = 0;
}
MyClass(int size)
{
this->size = size;
data = new int[size];
for (size_t i = 0; i < size; i++)
{
data[i] = rand();
}
}
MyClass(const MyClass& obj)
{
this->size = obj.size;
this->data = new int[size];
for (size_t i = 0; i < size; i++)
{
data[i] = obj.data[i];
}
}
MyClass operator+(const MyClass& obj)
{
MyClass temp;
temp.size = this->size + obj.size;
return temp.size;
}
friend ostream& operator<<(ostream& os, MyClass& obj);
~MyClass()
{
delete[]data;
}
};
ostream& operator<<(ostream & os, MyClass & obj)
{
os << obj.size;
return os;
}
int main()
{
MyClass a(5);
MyClass b(a);
MyClass c = a + b;
cout << c;
return 0;
}
I see several issues with your code:
your default constructor is not initializing data.
your operator+ is not attempting to copy values from this->data and obj.data into temp.data, thus leaving temp.data uninitialized.
your operator+ is returning the wrong MyClass object. It goes to the trouble of preparing a MyClass object named temp, and then completely discards temp upon exit. By passing temp.size to return, you are creating another MyClass object via your MyClass(int size) constructor, which generates all new random data. You need to instead return the temp object that you prepared. Then the compiler will either call your copy constructor to assign temp to c in main(), or it will optimize away the copy to let operator+ operate directly on c.
you are missing a copy assignment operator, per the Rule of 3. Nothing in your example actually invokes a copy assignment, but you need to implement the operator properly nonetheless. And if you are using C++11 or later, you should also follow the Rule of 5 as well, by adding a move constructor and a move assignment operator. However, if you can change your design to use std::vector instead of new[], then you can follow the Rule of 0 and let the compiler do all the hard work for you.
With that said, try something more like this:
#include <iostream>
#include <algorithm>
#include <utility> // C++11 and later only...
#include <cstdlib>
#include <ctime>
class MyClass
{
private:
int* data;
size_t size;
public:
MyClass(size_t size = 0) : data(NULL), size(size)
{
if (size > 0)
{
data = new int[size];
// in C++11 and later, consider using std::uniform_int_distribution instead of rand()!
std::generate(data, data + size, std::rand);
}
}
MyClass(const MyClass& obj) : data(NULL), size(obj.size)
{
if (size > 0)
{
data = new int[size];
std::copy(obj.data, obj.data + obj.size, data);
}
}
// C++11 and later only...
MyClass(MyClass&& obj) : data(NULL), size(0)
{
std::swap(size, obj.size);
std::swap(data, obj.data);
}
~MyClass()
{
delete[] data;
}
MyClass& operator=(const MyClass& rhs)
{
if (&rhs != this)
{
MyClass temp(rhs);
std::swap(size, temp.size);
std::swap(data, temp.data);
}
return *this;
}
MyClass& operator=(MyClass&& rhs)
{
MyClass temp(std::move(rhs));
std::swap(size, temp.size);
std::swap(data, temp.data);
return *this;
}
MyClass operator+(const MyClass& obj) const
{
MyClass temp;
temp.size = size + obj.size;
if (temp.size > 0)
{
temp.data = new int[temp.size];
std::copy(data, data + size, temp.data);
std::copy(obj.data, obj.data + obj.size, temp.data + size);
}
return temp;
}
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);
};
std::ostream& operator<<(std::ostream& os, const MyClass& obj)
{
os << obj.size;
for(size_t i = 0; i < obj.size; ++i)
{
os << " " << obj.data[i];
}
return os;
}
int main()
{
std::srand(std::time(0));
MyClass a(5);
MyClass b(a);
MyClass c = a + b;
std::cout << c;
return 0;
}
Which can be simplified to this:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
class MyClass
{
private:
std::vector<int> data;
public:
MyClass(size_t size = 0) : data(size)
{
std::generate(data.begin(), data.end(), std::rand);
}
MyClass operator+(const MyClass& obj) const
{
MyClass temp;
if (!data.empty() || !obj.data.empty())
{
temp.data.reserve(data.size() + obj.data.size());
temp.data.insert(temp.data.end(), data.begin(), data.end());
temp.data.insert(temp.data.end(), obj.data.begin(), obj.data.end());
}
return temp;
}
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);
};
std::ostream& operator<<(std::ostream& os, const MyClass& obj)
{
os << obj.size;
for(size_t i = 0; i < obj.size; ++i)
{
os << " " << obj.data[i];
}
return os;
}
int main()
{
std::srand(std::time(0));
MyClass a(5);
MyClass b(a);
MyClass c = a + b;
std::cout << c;
return 0;
}
If you can use std::vector, your code can become:
class MyClass {
private:
vector<int> data;
public:
MyClass(int size) {
data.resize(size);
for (size_t i = 0; i < size; i++) {
data[i] = rand();
}
}
MyClass operator+(const MyClass& obj) {
return data.size() + obj.data.size();
}
friend ostream& operator<<(ostream& os, MyClass& obj);
};
ostream& operator<<(ostream& os, MyClass& obj) {
os << obj.data.size();
return os;
}
Otherwise do not delete on an uninitialized pointer. (so either call new or set to nullptr).
You do not initialize the pointer in the default constructor, so do not call delete in the destructor.
Change the default constructor to set data to nullptr
MyClass() {
data = nullptr;
size = 0;
}
Also do not copy from an array when you have not initialized the pointer. When you set the size variable you are not creating the array. You should create a function called setSize and change your code to this:
MyClass(int size) {
data = nullptr;
setSize(size);
}
void setSize(int size) {
this->size = size;
delete[] data;
data = new int[size];
for (size_t i = 0; i < size; i++) {
data[i] = rand();
}
}
MyClass operator+(const MyClass& obj) {
MyClass temp;
temp.setSize(this->size + obj.size);
return temp.size;
}
Whenever you need to change the size, you should use the setSize function to make sure the array is created as well.
I'm constructed a simple container called buffer. When overloading the = operator i'm getting the following error
Exception thrown at 0x774AEBA5 (ntdll.dll) in init_list_test.exe:
0xC000000D: An invalid parameter was passed to a service or function.
Unhandled exception at 0x774AEBA5 (ntdll.dll) in init_list_test.exe:
0xC000000D: An invalid parameter was passed to a service or function.
Not really sure what this means.
Here is the code to reproduce:
#include "buffer.h"
int main()
{
buffer buf(10);
buffer test(10);
buf = test;
return 0;
}
in buffer.cpp
#include "buffer.h"
#include <iostream>
size_t buffer::get_size() const
{
return length;
}
buffer::buffer(size_t length) : start(new int[length]), length(length)
{
std::cout << length << +" size" << std::endl;
}
buffer::buffer(const buffer& rhs) : start(new int[length]), length(rhs.get_size())
{
std::copy(rhs.begin(), rhs.end(), start);
}
buffer& buffer::operator=(const buffer& rhs)
{
buffer temp_buff(rhs);
return temp_buff;
}
int* buffer::begin()
{
return start;
}
int* buffer::end()
{
return start + length;
}
const int* buffer::begin() const
{
return start;
}
const int* buffer::end() const
{
return start + length;
}
in buffer.h
#pragma once
class buffer {
int* start;
size_t length;
public:
size_t get_size() const;
explicit buffer(size_t size);
buffer(const buffer& rhs);
buffer& operator=(const buffer& rhs);
int* begin();
int* end();
const int* begin() const;
const int* end() const;
};
I saw on cppref that the offical way was to do something like this code below:
// assume the object holds reusable storage, such as a heap-allocated buffer mArray
T& operator=(const T& other) // copy assignment
{
if (this != &other) { // self-assignment check expected
if (other.size != size) { // storage cannot be reused
delete[] mArray; // destroy storage in this
size = 0;
mArray = nullptr; // preserve invariants in case next line throws
mArray = new int[other.size]; // create storage in this
size = other.size;
}
std::copy(other.mArray, other.mArray + other.size, mArray);
}
return *this;
}
However i wanted to use the copy ctor that i already designed since the code is very similar.
I'm trying to do a simple buffer and follow the RAII idiom
in main.cpp
int main()
{
buffer buf(5);
buffer test(10);
test = buf;
return 0;
}
in buffer.cpp
#include "buffer.h"
#include <iostream>
size_t buffer::get_size() const
{
return length;
}
buffer::buffer(size_t length) : start(new int[length]), length(length)
{
std::cout << length << +" size" << std::endl;
}
buffer::buffer(const buffer& rhs) : start(new int[rhs.get_size()]), length(rhs.get_size())
{
std::copy(rhs.begin(), rhs.end(), start);
}
buffer& buffer::operator=(const buffer& rhs)
{
buffer temp = rhs;
std::swap(*this, temp);
return *this;
}
int* buffer::begin()
{
return start;
}
int* buffer::end()
{
return start + length;
}
const int* buffer::begin() const
{
return start;
}
const int* buffer::end() const
{
return start + length;
}
buffer::buffer(buffer&& rhs) noexcept
{
*this = std::move(rhs);
}
buffer& buffer::operator=(buffer&& rhs) noexcept
{
if (this != &rhs) { // if this is not the rhs object
start = rhs.start;
length = rhs.length;
rhs.start = nullptr;
rhs.length = 0;
}
return *this;
}
buffer::~buffer()
{
delete[] start;
}
in buffer.h
#pragma once
class buffer {
size_t length;
int* start;
public:
size_t get_size() const;
explicit buffer(size_t size);
buffer(const buffer& rhs);
buffer& operator=(const buffer& rhs);
buffer& operator=(buffer&& rhs) noexcept;
buffer(buffer&& rhs) noexcept;
int* begin();
int* end();
const int* begin() const;
const int* end() const;
~buffer();
};
Now as you notice in main buf is a smaller size than test. My question is what happens to the memory allocated by test on the line above test=buf?
Does it ever get cleaned up? Or do main have to finish before it gets cleaned up.
The original array allocated by test will be swapped to the temp object in the copy assignment, and freed when that object goes out of scope.
But the move assignment has a memory leak, you have to free the old array before acquiring the new one from rhs.
And generally, I would recommend using unique_ptr instead of manual allocation.
I have my own vector class:
#include<iterator>
#include<memory>
#include<iostream>
#include<exception>
using std::cout;
using std::endl;
using std::size_t;
template <class T> class vector
{
public:
typedef T* iterator;
typedef const iterator const_iterator;
typedef size_t size_type;
typedef T value_type;
//------------------------------------------
vector() { create(); }
explicit vector(size_type n, const T& value = T{}) { create(n, value); }
vector(const vector& a) { create(a.begin(), a.end()); }
And operators:
vector& operator=(const vector& a)
{
if (&a == this) return *this;
uncreate();
create(a.begin(), a.end());
return *this;
}
vector& operator=(const vector&& a)
{
if (&a == this) return *this;
uncreate();
//NEED TO WRITE SOME CODE HERE
}
~vector() { uncreate(); } //destructor
size_type size() const { return avail - data; }
size_type capacity() const { return limit - data; }
And private class members:
private:
iterator data;
iterator avail;
iterator limit;
std::allocator<T> alloc;
//--------------------------------
void create()
{
data = avail = limit = nullptr;
}
And I should write operator = with the move (which I do not know how to write) and copy (which I have done) semantics. Can you help me?
vector& operator=(vector&& a) noexcept
{
a.swap(*this);
return *this;
}
void swap(vector& otherVector) noexcept
{
//swap all member variables here (I took some names that might not be the same, that you use)
std::swap(otherVector.capacity, capacity);
std::swap(otherVector.size, size);
std::swap(otherVector.buffer, buffer);
}
thats how I would write it (deleting is not needed,because it's swapped and so it will be deleted when the other vector goes out of scope)
obviously when you do not want a swap function you could aswell just put everything the swap function does into the move assignment operator ;)
just wrote a code for a template array class (I know its not finished yet), and trying to remember how to overload operators (meanwhile without special difficulties...).
Anyway, while thinking of how to implement operator[] I wondered what would happen if the index is outside the boundaries of the array... I'm pretty sure it is not possible for me to return a NULL (because of the return type), right? and if so, what should I return in case the index is out of boundries?
here's the code, most of it is redundant to my question, but it might help anyone who google's operators overloading so I post the complete code...
#ifndef __MYARRAY_H
#define __MYARRAY_H
#include <iostream>
using namespace std;
template <class T>
class MyArray
{
int phisicalSize, logicalSize;
char printDelimiter;
T* arr;
public:
MyArray(int size=10, char printDelimiter=' ');
MyArray(const MyArray& other);
~MyArray() {delete []arr;}
const MyArray& operator=(const MyArray& other);
const MyArray& operator+=(const T& newVal);
T& operator[](int index);
friend ostream& operator<<(ostream& os, const MyArray& ma)
{
for(int i=0; i<ma.logicalSize; i++)
os << ma.arr[i] << ma.printDelimiter;
return os;
}
};
template <class T>
T& MyArray<T>::operator[]( int index )
{
if (index < 0 || index > logicalSize)
{
//do what???
}
return arr[index];
}
template <class T>
const MyArray<T>& MyArray<T>::operator+=( const T& newVal )
{
if (logicalSize < phisicalSize)
{
arr[logicalSize] = newVal;
logicalSize++;
}
return *this;
}
template <class T>
const MyArray<T>& MyArray<T>::operator=( const MyArray<T>& other )
{
if (this != &other)
{
delete []arr;
phisicalSize = other.phisicalSize;
logicalSize = other.logicalSize;
printDelimiter = other.printDelimiter;
arr = new T[phisicalSize];
for(int i=0; i<logicalSize; i++)
arr[i] = other.arr[i];
}
return *this;
}
template <class T>
MyArray<T>::MyArray( const MyArray& other ) : arr(NULL)
{
*this = other;
}
template <class T>
MyArray<T>::MyArray( int size, char printDelimiter ) : phisicalSize(size), logicalSize(0), printDelimiter(printDelimiter)
{
arr = new T[phisicalSize];
}
#endif
operator[] generally does no bounds checking. Most of the standard containers that can have a range utilize a separate function, at(), which is range checked and throws a std::out_of_range exception.
You likely want to implement a const T& operator[] overload, too.