Implementing C++ assignment overloading can be error-prone if the object involves data on the memory heap.
In this blog post, I would like to quickly discuss how to implement exception safe C++ assignment overloading and how to apply the copy-and-swap idiom to implement it more elegantly.
C++ Assignment Overloading
Exception-Safe and Self-Assignment
Make sure the resource management inside the assignment overloading has taken throwing exceptions into account to prevent the data loss from the object. In the following example, we strictly follow allocate, populate and deallocate. If we deallocate the cstring member variable before allocate the new buffer, and allocating new buffer throws exception, we lose the cstring data forever.
String& operator=(const String& other) { // Ignore self assignment. // A little bit problematic here because of the data on the heap. // Need to overload == and the comparison might take a long time. // This step might just be skipped. if (this == &other) return *this; std::size_t n{std::strlen(other.cstring) + 1}; // The order matters to make sure the assignment is exception-safe. char* new_cstring = newchar[n]; // allocate std::memcpy(new_cstring, other.cstring, n); // populate delete[] cstring; // deallocate cstring = new_cstring; return *this; }
booloperator==(const String& other) constnoexcept { if (std::strlen(cstring) != std::strlen(other.cstring)) { returnfalse; } for (std::size_t i = 0; i < std::strlen(cstring); ++i) { if (cstring[i] != other.cstring[i]) { returnfalse; } } returntrue; }
Valgrind verified that there is no memory leak during runtime.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
$ g++ string.cpp -o string -std=c++14 $ valgrind --leak-check=full ./string ==56318== Memcheck, a memory error detector ==56318== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==56318== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==56318== Command: ./string ==56318== ==56318== ==56318== HEAP SUMMARY: ==56318== in use at exit: 0 bytes in 0 blocks ==56318== total heap usage: 4 allocs, 4 frees, 72,717 bytes allocated ==56318== ==56318== All heap blocks were freed -- no leaks are possible ==56318== ==56318== For lists of detected and suppressed errors, rerun with: -s ==56318== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Copy-And-Swap Idiom
A more elegant way of implementing assignment overloading is to apply the “copy-and-swap” idiom. We create a public or private noexcept swap member function and use the member function for the temporary copy of the assignment source.
String& operator=(const String& other) { // Ignore self assignment. // A little bit problematic here because of the data on the heap. // Need to overload == and the comparison might take a long time. // This step might just be skipped. if (this == &other) return *this; // copy-and-swap idiom. String temp_string{other}; swap(temp_string); return *this; }
// no exception is allowed for swap. voidswap(String& other)noexcept { char* temp_cstring = cstring; cstring = other.cstring; other.cstring = temp_cstring; }
booloperator==(const String& other) constnoexcept { if (std::strlen(cstring) != std::strlen(other.cstring)) { returnfalse; } for (std::size_t i = 0; i < std::strlen(cstring); ++i) { if (cstring[i] != other.cstring[i]) { returnfalse; } } returntrue; }
Valgrind verified that there is no memory leak during runtime.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
$ g++ string.cpp -o string -std=c++14 $ valgrind --leak-check=full ./string ==56745== Memcheck, a memory error detector ==56745== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==56745== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==56745== Command: ./string ==56745== ==56745== ==56745== HEAP SUMMARY: ==56745== in use at exit: 0 bytes in 0 blocks ==56745== total heap usage: 4 allocs, 4 frees, 72,717 bytes allocated ==56745== ==56745== All heap blocks were freed -- no leaks are possible ==56745== ==56745== For lists of detected and suppressed errors, rerun with: -s ==56745== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)