🎓 A1Learner
🔍
0 of 28 lessons completed

C++ Noexcept and Moves

noexcept is a promise written after the parameter list of a function that it will not throw. The promise changes how a vector grows, and the change shows up in move and copy counts.

A function that promises not to throw

The word noexcept after the parameter list states the promise. The function doubles its value and prints the result.

#include <iostream>

int twice(int value) noexcept {
  return value * 2;
}

int main() {
  std::cout << "Twice: " << twice(21) << "\n";
  return 0;
}

A noexcept move wins the growth race

Both vectors take five pushes of temporary boxes. The fast box counts twelve moves in total, five for the pushes and seven for the growth steps. The slow box counts seven copies, one set for each growth step.

#include <iostream>
#include <vector>

class FastBox {
public:
  static int moves;
  FastBox() {
  }
  FastBox(const FastBox& other) {
  }
  FastBox(FastBox&& other) noexcept {
    moves++;
  }
};

int FastBox::moves = 0;

class SlowBox {
public:
  static int copies;
  SlowBox() {
  }
  SlowBox(const SlowBox& other) {
    copies++;
  }
  SlowBox(SlowBox&& other) {
  }
};

int SlowBox::copies = 0;

int main() {
  std::vector<FastBox> fast;
  std::vector<SlowBox> slow;
  for (int i = 0; i < 5; i++) {
    fast.push_back(FastBox());
    slow.push_back(SlowBox());
  }
  std::cout << "Fast moves: " << FastBox::moves << "\n";
  std::cout << "Slow copies: " << SlowBox::copies << "\n";
  return 0;
}

A noexcept move assignment

The move assignment takes the value and empties the source by hand. Its noexcept promise lets other code rely on the transfer finishing.

#include <iostream>
#include <utility>

class Box {
public:
  Box() {
  }
  explicit Box(int value) {
    value_ = value;
  }
  Box& operator=(Box&& other) noexcept {
    value_ = other.value_;
    other.value_ = 0;
    return *this;
  }
  int value() const {
    return value_;
  }
private:
  int value_ = 0;
};

int main() {
  Box source(9);
  Box target;
  target = std::move(source);
  std::cout << "Target: " << target.value() << "\n";
  std::cout << "Source: " << source.value() << "\n";
  return 0;
}

Example

#include <iostream>
#include <vector>

class FastBox {
public:
  static int moves;
  FastBox() {
  }
  FastBox(const FastBox& other) {
  }
  FastBox(FastBox&& other) noexcept {
    moves++;
  }
};

int FastBox::moves = 0;

class SlowBox {
public:
  static int copies;
  SlowBox() {
  }
  SlowBox(const SlowBox& other) {
    copies++;
  }
  SlowBox(SlowBox&& other) {
  }
};

int SlowBox::copies = 0;

int main() {
  std::vector<FastBox> fast;
  std::vector<SlowBox> slow;
  for (int i = 0; i < 6; i++) {
    fast.push_back(FastBox());
    slow.push_back(SlowBox());
  }
  std::cout << "Fast moves: " << FastBox::moves << "\n";
  std::cout << "Slow copies: " << SlowBox::copies << "\n";
  return 0;
}

Key points

  • A function marked noexcept promises that it will not throw an exception.
  • If a noexcept function throws anyway, the program stops at once.
  • A vector grows by moving its elements into bigger storage.
  • The vector moves elements on growth only when the move constructor is noexcept.
  • When the move constructor can throw, the vector copies on growth instead, so a failed move cannot strand the elements.
  • Five pushes grow storage through sizes 1, 2, 4 and 8, so growth relocates 0, 1, 2 and then 4 elements.

In this example, both vectors take six pushes of temporary boxes. The fast box counts thirteen moves, six for the pushes and seven for the growth steps. The slow box still counts seven copies, because six pushes grow storage in the same steps as five.

Note

The counts in this lesson come from this compiler and its library. Other libraries may grow storage in different steps.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A vector is about to grow, and the element move constructor is not marked noexcept. What does the vector do with the stored elements?