2
\$\begingroup\$

My original deque implementation and efficiency question lacked move semantics. After ratchet freak provided an example of how to define them I decided to try adding them to my deque. The unit test + output below shows that the code is working.

However, my new question is: Are there edge cases in any of my move semantic functions that I may have forgotten, and is the data.~type(); necessary in the eNode<type>::operator=(type && other)? I'd also appreciate any feedback on my coding style.

template<class type>
class eDeque;
//*************************************************
// eNode
// to be used with friend class eDeque<type>
//*************************************************
template<class type>
class eNode {
 friend class eDeque<type>;
public :
// ... remainder of class definition (see referenced question)
 explicit eNode(type && data) : prev(nullptr), next(nullptr) { std::swap(this->data, data); }; // move constructor
 // DEBUG: destructor omitted because no heap allocation occurs
 eNode<type> & operator=(eNode<type> && other) { // move assignment
 data.~type(); 
 std::swap(this->data, other.data); 
 return *this;};
// ... remainder of class definition (see referenced question)
private:
 eNode<type> * prev;
 eNode<type> * next;
 type data;
};
template <class type>
class eDeque {
public:
// ... remainder of class definition (see referenced question)
 eDeque(eDeque<type> && other); // move constructor
 eDeque<type> & operator=(eDeque<type> && other); // move assignment
 void PushFront(type && data); // emplace and move
 void PushBack(type && data); // emplace and move
private:
 eNode<type> * front;
 eNode<type> * back;
 int nodeCount;
 // ... remainder of class definition (see referenced question)
};
//******************
// eDeque::eDeque
// move constructor
//******************
template <class type>
inline eDeque<type>::eDeque(eDeque<type> && other) : nodeCount(0), front(nullptr), back(nullptr) {
 std::swap(nodeCount, other.nodeCount);
 std::swap(front, other.front);
 std::swap(back, other.back);
}
//******************
// eDeque::operator=
// move assignment
//******************
template <class type>
inline eDeque<type> & eDeque<type>::operator=(eDeque<type> && other) {
 Clear();
 std::swap(nodeCount, other.nodeCount);
 std::swap(front, other.front);
 std::swap(back, other.back);
 return *this;
}
//******************
// eDeque::PushFront
// emplace and move
//******************
template <class type>
inline void eDeque<type>::PushFront(type && data) {
 eNode<type> * newFront;
 newFront = new eNode<type>(std::move(data));
 if (front == nullptr) {
 back = newFront;
 front = newFront;
 } else {
 newFront->prev = front;
 front->next = newFront;
 front = newFront;
 }
 nodeCount++;
}
//******************
// eDeque::PushBack
// emplace and move
//******************
template <class type>
inline void eDeque<type>::PushBack(type && data) {
 eNode<type> * newBack;
 newBack = new eNode<type>(std::move(data));
 if (back == nullptr) {
 back = newBack;
 front = newBack;
 }
 else {
 newBack->next = back;
 back->prev = newBack;
 back = newBack;
 }
 nodeCount++;
}

The following is the parts of the unit test added to the original source plus some context:

#include "Deque.h"
// iterate and print back to front
template<class type>
void PrintDeque(const eDeque<type> & de, const char * name) {
 eNode<int> * iterator;
 int i;
 printf("%s size == %i\n", name, de.Size());
 for (i = 0, iterator = de.Back(); iterator != nullptr; iterator = iterator->Next())
 printf("node %i == %i\n", i++, iterator->Data());
 printf("\n\n");
}
// iterate and increment by amount back to front
template<class type>
void IncrementData(const eDeque<type> & de, int amount) {
 eNode<int> * iterator;
 for (iterator = de.Back(); iterator != nullptr; iterator = iterator->Next())
 iterator->Data() += amount;
}
// unit test
int main() {
 int lValues[] = { 0, 1, 2, 3, 4, 5, 6 };
 printf("BEGIN eDeque COPY TESTS\n\n");
 eDeque<int> original;
 original.PushFront(lValues[0]);
 original.PushFront(lValues[1]);
 original.PushFront(lValues[2]);
 original.PushFront(lValues[3]);
 original.PushFront(lValues[4]);
 original.PushFront(lValues[5]);
 original.PushBack(lValues[6]);
 original.PopBack();
 original.PopFront();
 PrintDeque(original, "original");
 eDeque<int> B;
 printf("B.Size() < original.Size()\n");
 PrintDeque(B, "B_before_copyAssign");
 B = original; // copy assignment test with B.Size() < original.Size()
 printf("B = original\n");
 PrintDeque(B, "B result");
 eDeque<int> C;
 // these are now emplace and move calls
 C.PushBack(10);
 C.PushFront(11);
 C.PushBack(12);
 C.PushFront(13);
 C.PushBack(14);
 C.PushFront(15);
 C.PushBack(16);
 C.PushFront(17);
 printf("C.Size() > original.Size()\n");
 PrintDeque(C, "C_before_copyAssign");
 C = original; // copy assignment test with C.Size() > original.Size()
 printf("C = original\n");
 PrintDeque(C, "C result");
 // verify the original is intact
 printf("original deque intact\n");
 PrintDeque(original, "original");
 IncrementData(C, 5);
 printf("incremented each node of C by 5\n");
 PrintDeque(C, "C result");
 printf("B.Size() == C.Size()\n");
 B = C; // copy assignment test with B.Size() == C.Size()
 printf("B = C\n");
 PrintDeque(B, "B result");
// BEGIN eDeque move semantics testing
 eDeque<int> G = eDeque<int>(B); // copy construct an rvalue eDeque, then move construct G
 printf("eDeque<int> G = eDeque<int>(B) move construction\n");
 PrintDeque(G, "G result");
 IncrementData(B, 10);
 printf("incremented each node of B by 10\n");
 PrintDeque(B, "B result");
 G = eDeque<int>(std::move(B)); // move construct an rvalue eDeque, then move assign G (B becomes undefined/unspecified)
 printf("G = eDeque<int>(B) move assignment (B becomes unspecified)\n");
 PrintDeque(G, "G result");
 PrintDeque(B, "B result");
 int lValue = 666;
 printf("G.PushBack(std::move(lValue == 666)); G.PushFront(777) emplace and move\n");
 G.PushBack(std::move(lValue)); // emplace and move
 G.PushFront(777); // emplace and move
 PrintDeque(G, "G result");
// END eDeque move semantics testing
 printf("END eDeque COPY TESTS\n\n");
 printf("BEGIN eNODE COPY TESTS\n\n");
// BEGIN eNode move semantics testing
 eNode<int> H = eNode<int>(123); // move construction
 printf("eNode<int> H = eNode<int>(123) move construction result: H.Data() == %i\n\n", H.Data());
 H = eNode<int>(456); // move assignment
 printf("H = eNode<int>(456) move assignment result: H.Data() == %i, H.Next() == %p, H.Prev() == %p\n\n", H.Data(), H.Next(), H.Prev());
 printf("G.PushFront(std::move(H.Data()))\n");
 G.PushFront(std::move(H.Data())); // H.data now unspecified
 PrintDeque(G, "G result");
// END eNode move semantics testing
 printf("END eNODE COPY TESTS\n\n");
 return 0;
}

The following is the output of the relevant unit test code:

BEGIN eDeque COPY TESTS
original size == 5
node 0 == 0
node 1 == 1
node 2 == 2
node 3 == 3
node 4 == 4
B.Size() < original.Size()
B_before_copyAssign size == 0
B = original
B result size == 5
node 0 == 0
node 1 == 1
node 2 == 2
node 3 == 3
node 4 == 4
C.Size() > original.Size()
C_before_copyAssign size == 8
node 0 == 16
node 1 == 14
node 2 == 12
node 3 == 10
node 4 == 11
node 5 == 13
node 6 == 15
node 7 == 17 
C = original
C result size == 5
node 0 == 0
node 1 == 1
node 2 == 2
node 3 == 3
node 4 == 4
original deque intact
original size == 5
node 0 == 0
node 1 == 1
node 2 == 2
node 3 == 3
node 4 == 4
incremented each node of C by 5
C result size == 5
node 0 == 5
node 1 == 6
node 2 == 7
node 3 == 8
node 4 == 9
B.Size() == C.Size()
B = C
B result size == 5
node 0 == 5
node 1 == 6
node 2 == 7
node 3 == 8
node 4 == 9
eDeque<int> G = eDeque<int>(B) move construction
G result size == 5
node 0 == 5
node 1 == 6
node 2 == 7
node 3 == 8
node 4 == 9
incremented each node of B by 10
B result size == 5
node 0 == 15
node 1 == 16
node 2 == 17
node 3 == 18
node 4 == 19
G = eDeque<int>(B) move assignment (B becomes unspecified)
G result size == 5
node 0 == 15
node 1 == 16
node 2 == 17
node 3 == 18
node 4 == 19
B result size == 0
G.PushBack(std::move(lValue == 666)); G.PushFront(777) emplace and move
G result size == 7
node 0 == 666
node 1 == 15
node 2 == 16
node 3 == 17
node 4 == 18
node 5 == 19
node 6 == 777
END eDeque COPY TESTS
BEGIN eNODE COPY TESTS
eNode<int> H = eNode<int>(123) move construction result: H.Data() == 123
H = eNode<int>(456) move assignment result: H.Data() == 456, H.Next() == 0000000
0, H.Prev() == 00000000
G.PushFront(std::move(H.Data()))
G result size == 8
node 0 == 666
node 1 == 15
node 2 == 16
node 3 == 17
node 4 == 18
node 5 == 19
node 6 == 777
node 7 == 456
END eNODE COPY TESTS
asked Nov 17, 2016 at 21:20
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Try your code with eDeque<std::string> and then with eDeque<std::unique_ptr<int>>. \$\endgroup\$ Commented Nov 18, 2016 at 4:08
  • \$\begingroup\$ std::string worked fine, but std::unique_ptr<type> usage exposed my lack of a eNode ( eNode<type> && other) move constructor. Fixed. \$\endgroup\$ Commented Nov 18, 2016 at 16:54
  • 2
    \$\begingroup\$ eNode<std::string> H("you might need more than 24 characters here"); H = eNode<std::string>("123"); should not have worked, because of that explicit destructor call you were worried about. (You were right to be worried.) Where do you even use that assignment operator, though? I don't think your deque relies on that functionality (nor should it). \$\endgroup\$ Commented Nov 18, 2016 at 20:51

1 Answer 1

2
\$\begingroup\$

Credit to Quuxplusone for the test cases.

The test-case for std::unique_ptr<type> came up and it threw a bug-unearthing monkey-wrench in my design (as did std::string) during assignment operations.

I devised the following std::unique_ptr test to see if my explicit data.~type(); call is necessary and I noticed that std::unique_ptr seems to call its pointed-to object's destructor if the std::unique_ptr<T> & std::unique_ptr<T>::operator=(std::unique_ptr<T> && rhs) is used, and during copy construction.

#include <memory>
int GUID = 0; 
typedef struct T_s {
 const char * value;
 int id;
 T_s(const char * value) : value(value), id(GUID++) {
 printf("CONSTRUCT\t(GUID %i): %s\tat address %p\n\n", id, this->value, &(this->value)); 
 };
 ~T_s() { 
 printf("DESTRUCT\t(GUID %i): %s\tat address %p\n\n", id, this->value, &(this->value)); 
 };
} T;
void DestructorTest() {
 // default construction of T, default constructon of unique_ptr
 std::unique_ptr<T> A(new T("ctor")); 
 printf("A->value ==\t(GUID %i): %s\tat address %p\n\n", A->id, A->value, &(A->value));
 // default construction of T, copy construction of unique_ptr
 std::unique_ptr<T> B = std::make_unique<T>(T("cctor")); 
 printf("B->value ==\t(GUID %i): %s\tat address %p\n\n", B->id, B->value, &(B->value));
 // default construction of T, move assignment of unique_ptr
 B = std::make_unique<T>(T("move")); 
 printf("B->value ==\t(GUID %i): %s\tat address %p\n\n", B->id, B->value, &(B->value));
}

The output of which is:

CONSTRUCT (GUID 0): ctor at address 01188AF0
A->value == (GUID 0): ctor at address 01188AF0
CONSTRUCT (GUID 1): cctor at address 00F6F6A8
DESTRUCT (GUID 1): cctor at address 00F6F6A8
B->value == (GUID 1): cctor at address 01188A10
CONSTRUCT (GUID 2): move at address 00F6F698
DESTRUCT (GUID 1): cctor at address 01188A10
DESTRUCT (GUID 2): move at address 00F6F698
B->value == (GUID 2): move at address 01188A80
DESTRUCT (GUID 2): move at address 01188A80
DESTRUCT (GUID 0): ctor at address 01188AF0

As shown, the constructor only gets called 3 times, while the destructor gets called 5 times. This is because of temporary variables leaving scope in addition to std::unique_ptr's destructor calls.

I tried the copy-and-swap idiom for my eNode<type> assignment operator and it seems to have resolved the std::string and std::unique_ptr<int> crashes.

Here is the rewritten eNode<type> assignment operator (excerpted from its class declaration):

eNode(eNode<type> && other) : prev(nullptr), next(nullptr), data(std::move(other.data)) {}; // move constructor 
eNode(const eNode<type> & other) : prev(nullptr), next(nullptr), data(other.data) {}; // copy constructor
// the input copy calls the appropriate constructor first
eNode<type> & operator=(eNode<type> other) { 
 std::swap(data, other.data);
 return *this;
 };

SOME NOTES

  • A part of me doubts this would hold up if the template were eDeque<eDeque<int *>> or the like.
  • I agree with @Quuxplusone that direct eNode<type> reassignment is not how a deque should be used, but I was curious enough about memory management and move semantics that I had to try, for science. I've since marked the eNode<type> constructors that take eNode<type> parameters as deleted. I believe this will aid in preventing any accidental attempts at such a thing.
  • This answer will be edited if it turns out there are some strong exception safety or memory leak issues.
answered Nov 19, 2016 at 4:06
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.