Viewing noexcept application scenario from C + + vector expansion

Time:2021-9-11

C + + 11 provides keywordsnoexceptTo indicate that a function cannot — or does not intend to — throw an exception:

void foo() noexcept;             // a function specified as will never throw
void foo2() noexcept(true);      // same as foo
void bar();                      // a function might throw exception
void bar2() noexcept(false);     // same as bar

So we need to understand the following two points:

  • noexceptWhat are the advantages, such as performance, readability, and so on.
  • Do you need to use it heavily in your codenoexcept

Noexcept benefits

Let’s start with STD:: vector and take a look at the first point.

We know that vector has its own capacity when we callpush_backHowever, when the vector is full, the vector will apply for a larger space for the new container and copy the original elements in the container into the new container:

image

But what if an exception occurs when expanding elements?

  • An exception occurs when applying for a new space: the old vector remains in its original state, and the thrown exception is handled by the user.
  • Exceptions occur when copying elements: all elements that have been copied are released by the destructor of the element, the allocated space is released, and the thrown exceptions are handled by the user.

This expansion methodcomparePerfect. The upstream call will be maintained when there are exceptionspush_backThe original state.

But whycomparePerfect, because the expansion here is still copy. When there is a class in the vector and there are many resources, it will be very time-consuming. Therefore, C + + 11 introduces a new feature:move, it will “steal” resources from the old elements to the new elements (students who are not familiar with move can check the information themselves, which will not be discussed here). Applied to the scene of vector expansion: when the moving copy constructor of the element in the vector isnoexceptInstead of using the copy method, the vector uses the move method to put the elements of the old container into the new container:

image

utilizemoveDue to the characteristics of exchange class resource ownership, the efficiency of using vector to expand capacity is greatly improved, but what to do when an exception occurs:
The state of the original container has been destroyed, and the resources of some elements have been stolen. Recovery greatly increases the complexity and unpredictability of your code. So only if the element in the vectormove constructoryesnoexceptWhen the vector is expanded, the move method will be adopted to improve the performance.

Just summarized the use ofnoexceptHow to improve vector capacity expansion. actually,noexceptIt is also widely used inswapFunction summove assignmentIn, the principle is the same.

Noexcept usage scenario

It’s mentioned abovenoexceptAvailable scenarios:

  • move constructor
  • move assignment
  • swap

Many people’s first thought may be: my function obviously won’t throw exceptions now, and then it’s declarednoexceptThe compiler can generate more efficient code, so add it if you can. But is this the case?

To discuss this issue clearly, we first need to know the following points:

  • Functions themselves do not throw exceptions, but it does not mean that their internal calls will not throw exceptions, and the compiler will not provide the relationship between the caller and the calleenoexceptConsistency check, such as the following code, is legal:
void g(){
    ...       //some code
}
void f() noexcept
{
    … 			//some code
    g();
}
  • When a declaration isnoexceptWhen the function of throws an exception, the program will be terminated and STD:: terminate() will be called;

Therefore, when the internal call of our code is complex, the link is long, and it is possible to add a new feature at any time, we add the function too earlynoexceptIt may not be a good choice becausenoexceptOnce added, it will become difficult to remove it later: the caller may see that your function is declared as noexcept, and the caller will also declare as noexceptnoexcept。 But when you put the functionnoexceptWhen the caller’s code is removed but not modified, the program will terminate when an exception is thrown to the caller.

At present, the mainstream view is:

  • Add noexcept
    • The function has been declared asthrow()
    • The three situations mentioned above: move constructor, move assignmemt and swap. If these implementations do not throw exceptions, be sure to usenoexcept
    • For example, get class member variables, simple operations of class member variables, etc. The following are several member functions in the forward iterator of STL:
# if __cplusplus >= 201103L
#  define _GLIBCXX_NOEXCEPT noexcept
# else
#  define _GLIBCXX_NOEXCEPT

 reference
      operator*() const _GLIBCXX_NOEXCEPT
      { return *_M_current; }

      pointer
      operator->() const _GLIBCXX_NOEXCEPT
      { return _M_current; }

      __normal_iterator&
      operator++() _GLIBCXX_NOEXCEPT
      {
	++_M_current;
	return *this;
      }

      __normal_iterator
      operator++(int) _GLIBCXX_NOEXCEPT
      { return __normal_iterator(_M_current++); }
  • No noexcept
    Do not add other functions except those to be added abovenoexceptCan.

Finally, let’s take a look at how vector implements utilizationnoexcept move constructorCapacity expansion andmove constructorDeclarenoexceptPerformance impact on capacity expansion.

How to realize utilizationnoexcept move constructorCapacity expansion

We won’t stick a large piece of code here. The implementation of each platform may be different. We only focus on how the vector judges the callcopy constructorstillmove constructorof

The core technologies used include:

  • type trait
  • iterator trait
  • move iterator
  • std::forward

Core code:

template ::value_type>::value,
                                  _Iterator, move_iterator>::type>
inline _GLIBCXX17_CONSTEXPR _ReturnType __make_move_if_noexcept_iterator(_Iterator __i) {
  return _ReturnType(__i);
}

template 
struct __move_if_noexcept_cond
    : public __and_>, is_copy_constructible>::type {};

Used heretype traitanditerator traitJoint judgment: if the element hasnoexcept move constructor, thenis_nothrow_move_constructible=1 => __move_if_noexcept_cond=0 => __make_move_if_noexcept_iteratorReturn amove iterator。 heremove iteratorThe iterator adapter is also a new feature of C + + 11, which is used to convert any processing of underlying elements into a move operation, for example:

std::list s;
std::vector v(make_move_iterator(s.begin()),make_move_iterator(s.end()));     // make_ move_ The iterator returns an STD:: move_ iterator

Then the upstream uses the generatedmove iteratorLoop element move:

{
  for (; __first != __last; ++__first, (void)++__cur) std::_Construct(std::__addressof(*__cur), *__first);
  return __cur;
}

template 
inline void _Construct(_T1 *__p, _Args &&... __args) {
  ::new (static_cast(__p)) _ T1(std::forward(__args)...);      // Actual copy (or move) element
}

among_ConstructIs the function of the actual copy (or move) element.The key point here is: dereference the move iterator and return an R-value reference., that’s the guarantee, when__firstType ismove iteratorWhen, use_T1(std::forward(__args)...Called only after “perfect forwarding”_T1Typemove constructor, the generated new object is placed in the new vector__pAddress.

To sum up, the process is:

  • utilizetype traitanditerator traitGenerate a pointer to the old containernormal iteratorperhapsmove iterator
  • The loop moves the elements of the old container to the new container. If the point to the old container ismove iterator, then dereference will return an R-value reference and the element’smove constructor, otherwise callcopy constructor

You can use the following simple code to debug on your own platform:

class A {
 public:
  A() { std::cout << "constructor" << std::endl; }
  A(const A &a) { std::cout << "copy constructor" << std::endl; }
  A(const A &&a) noexcept { std::cout << "move constructor" << std::endl; }
};

int main() {
  std::vector v;
  for (int i = 0; i < 10; i++) {
    A a;
    v.push_back(a);
  }

  return 0;
}

noexcept move constructorImpact on Performance

In this article, C + + noexcept and move constructors effect on performance in stl containers introduces the impact of noexcept move constructor on time consumption and memory. It will not be repeated here. Those interested can try it by themselves.

image

reference material:

  • When to Use noexcept And When to Not
  • Does noexcept improve performance?
  • EffectiveModernCppChinese Item14
  • How should the noexcept identifier and operator of C + + 11 be used correctly?

(end)

Friends can take note of my official account and get the most timely updates.

image

Recommended Today

Notes on basic learning of ruby metaprogramming

Note 1:The code contains variables, classes and methods, which are collectively referred to as language construct. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # test.rb class Greeting  def initialize(text)   @text = text  end    def welcome   @text  end end my_obj = Greeting.new(“hello”) […]