Featured image of post Noexcept Vectors

Noexcept Vectors

Using noexcept with vectors

Containers have a quirk, let’s look at it.

Noexcept

Consider a hypothetical case of a 2D Point class, possibly used for math or graphics.

 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
#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using std::vector;

struct Point
{
  int x = 0;
  int y = 0;

  Point() {
    cout << "default ctor" << endl;
  }

  Point(int x_, int y_)
      : x{x_}
      , y{y_}
  {
    cout << "param ctor" << endl;
  }

  Point(const Point&) {
    cout << "copy ctor" << endl;
  }

  Point& operator=(const Point&) {
    cout << "copy assign" << endl;
    return *this;
  }

  Point(Point&&) {
    cout << "move ctor" << endl;
  }

  Point& operator=(Point&&) {
    cout << "move assign" << endl;
    return *this;
  }
};

int main()
{
    vector<Point> points;

    for (int i = 0; i < 5; i++)
    {
        cout << "Loop " << (i + 1) << endl;

        points.emplace_back(1, 2);

        cout << endl;
    }
}

If we create and fill a vector of these points as in the example code above then we get the following output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Loop 1
param ctor

Loop 2
param ctor
copy ctor

Loop 3
param ctor
copy ctor
copy ctor

Loop 4
param ctor
copy ctor
copy ctor
copy ctor

Loop 5
param ctor
copy ctor
copy ctor
copy ctor
copy ctor

We see the copy constructor is called, not the move constructor.

This could be very wasteful for larger data types like image containers.

The solution is to mark the move ctor and move assign as noexcept.

 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
#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using std::vector;

struct Point
{
  int x = 0;
  int y = 0;

  Point() {
    cout << "default ctor" << endl;
  }

  Point(int x_, int y_)
      : x{x_}
      , y{y_}
  {
    cout << "param ctor" << endl;
  }

  Point(const Point&) {
    cout << "copy ctor" << endl;
  }

  Point& operator=(const Point&) {
    cout << "copy assign" << endl;
    return *this;
  }

  Point(Point&&) noexcept {
    cout << "move ctor" << endl;
  }

  Point& operator=(Point&&) noexcept {
    cout << "move assign" << endl;
    return *this;
  }
};

int main()
{
    vector<Point> points;

    for (int i = 0; i < 5; i++)
    {
        cout << "Loop " << (i + 1) << endl;

        points.emplace_back(1, 2);

        cout << endl;
    }
}

The only change here is that the move constructor and move assignment operator have been marked noexcept.

This then gives the following output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Loop 1
param ctor

Loop 2
param ctor
move ctor

Loop 3
param ctor
move ctor
move ctor

Loop 4
param ctor
move ctor
move ctor
move ctor

Loop 5
param ctor
move ctor
move ctor
move ctor
move ctor

Cover Photo

Credit: Guillermo Alvarez