Effective STL ITEM 43: Priority use STL generic algorithm to replace handwriting cycles

zhaozj2021-02-16  48

STL generic algorithm vs. handwritten cycle

Scott Meyers

Prepare optimization? Don't be so urgent. Scott is trying to let you believe the library function better than you write.

-------------------------------------------------- -----------------------------

This article originated from a book that will be published. S. Meyers, Effective Stl: 50 Specific Ways To Improve Your Use of the Standard Template Library, modified from Item 26-28 (WQ note, CUJ above, should be Item 43). ADDISON-WESLEY. Issue: Permission of Pearson Education, Inc]

Each generic algorithm accepts at least one pair of options to indicate the element interval that will be operated. For example, Min_Element () finds the smallest value in this section, and Accumulate () makes some forms of overall summation operations in the interval, partition () segmentates elements within the interval to satisfy and not satisfy certain Two parts of the decision condition. When the generic algorithm is executed, they must check each element in the interval of it, and is performed in the way you expect: from the starting point of the interval to the end point. There are some generic algorithms, such as Find () and Find_if (), may return before traversal, but even these generic algorithms, there is a loop inside. After all, even find () and find_if () must also determine the elements they find after viewing each element in this interval.

Therefore, the extensive algorithm is a loop. In addition, the STL generic algorithm involves a wide range of faces, which means that many tasks you have to use to use loops, and now you can use the generic algorithm to achieve. For example, there is a Widget class that supports RedRaw ().

Class widget {

PUBLIC:

...

Void Redraw () const;

...

}

Also, you want RedRaw all Widget objects in a list, you may use such a loop:

List LW;

...

For (List :: item i =

Lw.begin ();

I! = lw.end (); i) {

I-> redraw ();

}

But you can also use for_each () generic algorithm:

For_each (lw.begin (), lw.end (),

MEM_FUN_REF (& Widget :: RedRaw);

For many C programmers, use the cycle than the idea of ​​calling generic algorithms, and read solutions are more comfortable than making MEM_FUN_REF and widget :: RedRaw. However, this article will explain that the generic algorithm is more preferable. In fact, this article will prove that call generic algorithm is usually more superior to handwritten cycles. why?

Three reasons:

l Efficiency: The generic algorithm is usually efficient than the cycle.

l Propagation: It is more likely to generate an error while writing a generic algorithm.

l Maintainability: The generic algorithm usually makes the code cleaner and intuitive compared to the corresponding explicit cycle.

The following sections will be exemplified.

From the viewpoint of efficiency, the generic algorithm defeated explicit cycles, two main factors, and a secondary factor in three aspects. The secondary factor is to eliminate excess calculations. Look back and look at the loop we just wrote:

For (List :: item i = lw.begin ();

i! = lw.end ();

i) {

I-> redraw ();

}

I have highlighted the loop termination test statement to emphasize that each cycle is checked with lw.end (). That is to say, each cycle must call the function List :: end (). But we don't need to call end () more than one, because we are not preparing to modify this List, is enough to call the end (). And we turn to see the generic algorithm, you can see that only the correct value of the End () function is:

// this call evaluates lw.end () exactly

// ONCE

For_each (lw.begin (), lw.end (),

MEM_FUN_REF (& Widget :: RedRaw);

With the mind, the STL's implementation knows Begin () and end () (and similar functions, such as size ()) is very frequent, so it is most efficient as much as possible. Almost certainly inlines them, and encoding the most compilers can avoid repetition calculations (by setting the calculation result (this optimization)). However, experience shows that this is not always successful, and when it is not successful, the avoidance of repetition calculations is sufficient to make generic algorithms have performance advantages than handwritten cycles.

But this is just a secondary factor affecting performance. The first primary influencing factor is that the implementation of the library can use them to know the specific implementation of the container, and the user cannot optimize the code. For example, the elements in Deque are typically stored on an array of one or more fixed sizes (internal). The pointer-based traversal is faster than the selector, but only the implementation of the library can use the pointer-based traversal because only they know the size of the internal array and how to move the next one from a number of components. There are some implementation versions of STL containers and generic algorithms that are specifically considered their demineral data structure, and it is known that such implementation is 20% higher than "usual".

The second main factor is that in addition to the most negligible algorithm, all the mathematical algorithms used by the STL generic algorithm are more complicated than the algorithms that the general C programmers can get, and sometimes much more complicated. It is impossible to go beyond Sort () and its universal algorithm (for example, stable_sort (), nth_element (), etc.); Search algorithms for sequential intervals (for example, binary_search (), loc_bound (), etc.) quite perfect; even It is very ordinary task, such as destroying elements from a vector, deque or array, using the Erase-Remove usual method more efficient than the loop written by most programmers.

If the efficiency is not convinced, maybe you are more willing to accept the correctness based on the correctness. When writing a cycle, a more trouble is to ensure that the selected subsection (a) used is valid, and (b) pointing to where you expect. For example, it is assumed to have an array, you want to get every element of it, add 41 above, then insert the result from the front end into a Deque. Use loops, you may write this:

// c API: This function takes a pointer

// TO an Array of At Most Arraysize

// Doubles and Writes Data to it. IT

// Returns the number of doubles written.

SIZE_T FILLARRAY (double * parray); // Create Local Array of Max Possible Size

Double Data [MAXNUMDOUBLES];

// Create Deque, Put Data INTO IT

Deque D;

...

// Get Array Data from API

SIZE_T NUMDOUBLES =

Fillarray (Data, MaxNumdouboard);

// for Each I in data, INSERT DATA [I] 41

// at The Front of D; This Code Has A Bug!

For (size_t i = 0; i

D.insert (D.Begin (), DATA [i] 41);

}

This can be implemented, as long as you can satisfy the inserted elements are in the reverse sequence. Because each insertion point is d.Begin (), the last one is inserted, will be on the front of the Deque!

If this is not what you want (still admitted, it is definitely not what you want), you may want to modify this:

// Remember D'S Begin Iterator

Deque :: item insertlocation = d.begin ();

// INSERT DATA [I] 41 AT INSERTLOCATION, THEN

// increment INSERTLOCATION; this code is also buggy!

For (size_t i = 0; i

D. Insert (InsertLocation , Data [i] 41);

}

It looks like a win-win situation, it doesn't just add the choice indicating the insertion position, and avoids the call to becom () (this eliminates the secondary factor affecting efficiency). Oh, this method has fallen into another problem: it leads to the "undefined" result. Each time you call devE :: INSERT (), will cause all the selection of the DEQUE inside, including the above INSERTLOCATION. After the first call insert (), INSERTLOCATION becomes invalid, and the later loop can produce any behavior (Are ALOWED TOETTRIGHT to LOONEYLAND).

Take note of this problem, you may do this:

Deque :: item insertlocation =

D. ragin ();

// Update InsertLocation EACH TIME

// Insert Is Called to Keep The Itrator Valid,

// instruction IT

For (size_t i = 0; i

INSERTLOCATION =

D. Insert (InsertLocation, Data [i] 41);

insertlocation;

}

Such a code does complete the function of your masses, but I will reach this step! And call generic algorithm TRANSFORM () compare:

// Copy All Elements from data to the

// Front of D, Adding 41 to EACH

Transform (Data, Data NumdouBles,

INSERTER (D, D. Segin ()),

BIND2ND (Plus (), 41);

This "Bind2nd (Plus (), 41)" may spend some time to understand (especially if the STL's Bind family is used), but only the kind of harassment related to the selection is to point out the source interval. The starting point and the end point (and this will never be a problem) and ensure that INSERTER is used on the starting point of the destination interval. Actual experience shows that the correct initial selection is often easier for the source interval and destination intervals, at least more easily than to ensure that the cyclic body is not intended to be inadvertently, it is much more easily.

Because you must pay attention to whether you are not properly manipulated or invalid before using the selected child, it is much more representative. Assuming that the use invalid selection will cause the "undefined" behavior, it is assumed that "undefined" behavior during development and testing HAS a Nasty Habit of Failing to show itself, why do you unnecessary danger? Throw the selection to the generic algorithm, let them consider the various strange behaviors when manipulating the selection.

I have explained why generic algorithms can be more efficient than handwritten cycles, and also describe why cycles will be difficult to walk in a thorns associated with chosen, while generic algorithms are avoiding this. If you are lucky, you are now a generals of generic algorithms. However, luck is not enough, before I rest, I want to make more sure. Therefore, let us continue to travel to the code clarity. Finally, it is best for software to be the clearest software, the best software, can be most fun to enhance, maintain and apply software for new environments. Although it is accustomed to cycling, generic algorithms have advantages in this long-term competition.

The key is the power of the name of the name. There are about 70 extensive algorithms in STL, with a total of 100 different function templates (each overload is calculated). Each generic algorithm completes some carefully defined tasks, and there is reason to think that professional C programmers know (or should go see what each generic algorithm has completed. Therefore, when the programmer calls Transform (), they believe that each element in the interval has a function, and the result will be written to another place. When the programmer calls Replace_IF (), he (she) knows that the object that meets the rules in the interval will be modified. When calling partition (), he (she) understands all objects that satisfy the judgment conditions will be gathered together. The name of the STL generic algorithm conveys a lot of semantic information, which makes them more than the casual loop.

The name of the generic algorithm suggests its function. "For", "while" and "do" can't do this. In fact, this is true for all components of the standard C language or C language runtime. There is no doubt that you can implement strlen (), memset () or bsearch (), but you won't do this. Why not? Because (1) already helps you achieve them, there is no need to do itself again; (2) The name is the standard, so everyone knows what they do; and (3) you guess the library The implementator knows some tips you don't know, so you don't want to miss the skilled library implementors to provide optimization. As you don't write your own version of Strlen () and other functions, it also does not truly use loops to implement an equivalent version of the existing STL generic algorithm.

I hope that the story is over, because I think this is very persuasive. Oh, this is a tale what refuses to Go Gentle Into That Good Night. The name of the generic algorithm is much more meaningful than the loop cycle. This is the fact, but it is more likely to use the loop to make people understand the operations on the selection. For example, it is assumed that you want to find out the first element of the first bit of X and Y small than Y. This is the implementation of cycles: Vector v;

INT X, Y;

...

// ipiL ann

// appropriate value is found or

// v.end () is Reached

Vector :: item i = v.begin ();

For (; i! = v.end (); i) {

IF (* i> x && * i

}

// i now points to the value

// or is the same as v.end ()

Put the same logic to find_if () is possible, but you need to use a non-standard functionor, such as SGI's Compose2 [Note 1]:

// Find the first value Val where the first value

// "and" of val> x and val

Vector Iterator i =

Find_if (v.begin (), v.end (),

Compose2 (Logical_and (),

Bind2nd (Greater (), x),

BIND2ND (Less (), y)));

Even if there is no non-standard element, many programmers will oppose that it is far less clear, I have to agree with this view.

Find_if () calls can not be so complicated, as long as the test logic is encapsulated into a separate Functor (that is, the Operator () member function class):

Template

Class BetWeenValues:

Public std :: unary_function {

PUBLIC:

// Have the ctor save the

// Values ​​to Be Between

Betweenvalues ​​(Const T & LowValue,

Const T & HighValue)

: LowVal (LowValue), HIGHVAL (HIGHVALUE)

{}

// Return WHether Val IS

// Between the Saved Values

BOOL Operator () (Const T & Val) Const

{

Return Val> LowVal && Val

}

Private:

T lowval;

T highval;

}

...

Vector Iterator i =

Find_if (v.begin (), v.end (),

BetWeenValues ​​ (x, y));

But this method has its own defects. First, create a BetWeenValues ​​template to make more work more than write cyclicers. On the number of lights. Circulation: 1 line; BetWeenValues ​​Template: 24 lines. It is too uncomfortable. Second, find_if () is looking for something detained from the call, you must really understand this call to Find_if (), you must also view the definition of BetWeenValues, but BetWeenValues ​​must be defined in calling find_if () Outside of the function. If you try to make BetWeenValues ​​inside this function, like this, // beginning of function

{

...

Template

Class BetWeenValues:

Public std :: unary_function {...};

Vector :: item i =

Find_if (v.begin (), v.end (),

BetWeenValues ​​ (x, y));

...

}

// end of function

You will find that the compilation is not passed, because the template cannot declare in the function inside the function. If you try to use the class instead of the template, avoid this problem.

// Beginning of Function

{

...

Class BetWeenValues:

Public st :: unary_function {...};

Vector Iterator i =

Find_if (v.begin (), v.end (),

BetWeenValues ​​(X, Y));

...

}

// end of function

You will find that it is still poor, since the class defined inside the function is a local class, and the local class cannot be bound to the Type parameter of the template (such as the FUNCTOR type required by Find_IF ()). Very disappointed, the Functor class and the Functor class cannot be defined inside the function, whether it is more convenient to implement.

In the long-term compass of generic functions and handwriting cycles, the bottom line of code clarity is: This is entirely on what you want to do in the loop. If you have to do, generic algorithms have been provided, or very close to it, call generic algorithms clearer. If the things you have to do in the loop are very simple, but when you call generic algorithms, you have to use a Bind family and Adapter or an independent Functor class. You may still write a loop. Finally, if you do things in the cycle are quite long or quite complicated, balance once again tends again to generic algorithms. Long, complex usually always encapsulates into a separate function. As long as you put the cyclic body into a stand-alone function, you can almost always find the method to pass this function to a generic algorithm (usually for_each ()) so that the final code is straightforward.

If you agree to call generic algorithms usually better than your handwritten loop, if you agree that the member function that acts on a range is better than the cyclic calling function [Note 2], an interesting conclusion occurs. : The cycle in the C exquisite program using the STL container is much more than the equivalent program that does not use STL. This is a good thing. As long as you can replace low-level vocabulary (such as for, while and do) with high-level terms (such as insert (), Find () and for_each ()), we upgrade the abstract level of the software and thus make it more Easy to implement, documentation, enhance, and maintenance.

Note and reference

[1] To Learn More About Compose2, Consult The SGI STL Website () Or Matt Austern's Book, Generic Programming and The STL (Addison-Wesley, 1999). [2] Range member functions are container member functions such as insert, erase, and assign that take two iterators specifying a range to eg, insert, erase, or assign. A single call to a range member is generally much more efficient than a hand -written loop that does The Same Thing. For Details, Consult Item 5 of Effective STL.

转载请注明原文地址:https://www.9cbs.com/read-26704.html

New Post(0)