C++ Exception-Safe Assignment Overloading

Introduction

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <cstddef>
#include <cstring>
#include <iostream>

class String
{

public:
String(const char* s = "") : String(s, std::strlen(s) + 1) {}

String(const String& other) : String(other.cstring) {}

~String() { delete[] cstring; }

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 = new char[n]; // allocate
std::memcpy(new_cstring, other.cstring, n); // populate
delete[] cstring; // deallocate
cstring = new_cstring;
return *this;
}

bool operator==(const String& other) const noexcept
{
if (std::strlen(cstring) != std::strlen(other.cstring))
{
return false;
}
for (std::size_t i = 0; i < std::strlen(cstring); ++i)
{
if (cstring[i] != other.cstring[i])
{
return false;
}
}
return true;
}

operator const char*() const noexcept { return cstring; }

private:
String(const char* s, std::size_t n) : cstring(new char[n])
{
std::memcpy(cstring, s, n);
}

char* cstring;
};

int main()
{
String str_1{"abcd"};
String str_2{"def"};
str_1 = str_2;
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <cstddef>
#include <cstring>
#include <iostream>

class String
{

public:
String(const char* s = "") : String(s, std::strlen(s) + 1) {}

String(const String& other) : String(other.cstring) {}

~String() { delete[] cstring; }

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.
void swap(String& other) noexcept
{
char* temp_cstring = cstring;
cstring = other.cstring;
other.cstring = temp_cstring;
}

bool operator==(const String& other) const noexcept
{
if (std::strlen(cstring) != std::strlen(other.cstring))
{
return false;
}
for (std::size_t i = 0; i < std::strlen(cstring); ++i)
{
if (cstring[i] != other.cstring[i])
{
return false;
}
}
return true;
}

operator const char*() const noexcept { return cstring; }

private:
String(const char* s, std::size_t n) : cstring(new char[n])
{
std::memcpy(cstring, s, n);
}

char* cstring;
};

int main()
{
String str_1{"abcd"};
String str_2{"def"};
str_1 = str_2;
}

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)
Author

Lei Mao

Posted on

06-05-2022

Updated on

06-05-2022

Licensed under


Comments