What is the correct way of using C++11's range-based for?

后端 未结 4 921
面向向阳花
面向向阳花 2020-11-22 10:04

What is the correct way of using C++11\'s range-based for?

What syntax should be used? for (auto elem : container), or for (auto&

4条回答
  •  广开言路
    2020-11-22 10:52

    Let's start differentiating between observing the elements in the container vs. modifying them in place.

    Observing the elements

    Let's consider a simple example:

    vector v = {1, 3, 5, 7, 9};
    
    for (auto x : v)
        cout << x << ' ';
    

    The above code prints the elements (ints) in the vector:

    1 3 5 7 9
    

    Now consider another case, in which the vector elements are not just simple integers, but instances of a more complex class, with custom copy constructor, etc.

    // A sample test class, with custom copy semantics.
    class X
    {
    public:
        X() 
            : m_data(0) 
        {}
    
        X(int data)
            : m_data(data)
        {}
    
        ~X() 
        {}
    
        X(const X& other) 
            : m_data(other.m_data)
        { cout << "X copy ctor.\n"; }
    
        X& operator=(const X& other)
        {
            m_data = other.m_data;       
            cout << "X copy assign.\n";
            return *this;
        }
    
        int Get() const
        {
            return m_data;
        }
    
    private:
        int m_data;
    };
    
    ostream& operator<<(ostream& os, const X& x)
    {
        os << x.Get();
        return os;
    }
    

    If we use the above for (auto x : v) {...} syntax with this new class:

    vector v = {1, 3, 5, 7, 9};
    
    cout << "\nElements:\n";
    for (auto x : v)
    {
        cout << x << ' ';
    }
    

    the output is something like:

    [... copy constructor calls for vector initialization ...]
    
    Elements:
    X copy ctor.
    1 X copy ctor.
    3 X copy ctor.
    5 X copy ctor.
    7 X copy ctor.
    9
    

    As it can be read from the output, copy constructor calls are made during range-based for loop iterations.
    This is because we are capturing the elements from the container by value (the auto x part in for (auto x : v)).

    This is inefficient code, e.g., if these elements are instances of std::string, heap memory allocations can be done, with expensive trips to the memory manager, etc. This is useless if we just want to observe the elements in a container.

    So, a better syntax is available: capture by const reference, i.e. const auto&:

    vector v = {1, 3, 5, 7, 9};
    
    cout << "\nElements:\n";
    for (const auto& x : v)
    { 
        cout << x << ' ';
    }
    

    Now the output is:

     [... copy constructor calls for vector initialization ...]
    
    Elements:
    1 3 5 7 9
    

    Without any spurious (and potentially expensive) copy constructor call.

    So, when observing elements in a container (i.e., for read-only access), the following syntax is fine for simple cheap-to-copy types, like int, double, etc.:

    for (auto elem : container) 
    

    Else, capturing by const reference is better in the general case, to avoid useless (and potentially expensive) copy constructor calls:

    for (const auto& elem : container) 
    

    Modifying the elements in the container

    If we want to modify the elements in a container using range-based for, the above for (auto elem : container) and for (const auto& elem : container) syntaxes are wrong.

    In fact, in the former case, elem stores a copy of the original element, so modifications done to it are just lost and not stored persistently in the container, e.g.:

    vector v = {1, 3, 5, 7, 9};
    for (auto x : v)  // <-- capture by value (copy)
        x *= 10;      // <-- a local temporary copy ("x") is modified,
                      //     *not* the original vector element.
    
    for (auto x : v)
        cout << x << ' ';
    

    The output is just the initial sequence:

    1 3 5 7 9
    

    Instead, an attempt of using for (const auto& x : v) just fails to compile.

    g++ outputs an error message something like this:

    TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
              x *= 10;
                ^
    

    The correct approach in this case is capturing by non-const reference:

    vector v = {1, 3, 5, 7, 9};
    for (auto& x : v)
        x *= 10;
    
    for (auto x : v)
        cout << x << ' ';
    

    The output is (as expected):

    10 30 50 70 90
    

    This for (auto& elem : container) syntax works also for more complex types, e.g. considering a vector:

    vector v = {"Bob", "Jeff", "Connie"};
    
    // Modify elements in place: use "auto &"
    for (auto& x : v)
        x = "Hi " + x + "!";
    
    // Output elements (*observing* --> use "const auto&")
    for (const auto& x : v)
        cout << x << ' ';
    

    the output is:

    Hi Bob! Hi Jeff! Hi Connie!
    

    The special case of proxy iterators

    Suppose we have a vector, and we want to invert the logical boolean state of its elements, using the above syntax:

    vector v = {true, false, false, true};
    for (auto& x : v)
        x = !x;
    

    The above code fails to compile.

    g++ outputs an error message similar to this:

    TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
     type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
    ce {aka std::_Bit_reference}'
         for (auto& x : v)
                        ^
    

    The problem is that std::vector template is specialized for bool, with an implementation that packs the bools to optimize space (each boolean value is stored in one bit, eight "boolean" bits in a byte).

    Because of that (since it's not possible to return a reference to a single bit), vector uses a so-called "proxy iterator" pattern. A "proxy iterator" is an iterator that, when dereferenced, does not yield an ordinary bool &, but instead returns (by value) a temporary object, which is a proxy class convertible to bool. (See also this question and related answers here on StackOverflow.)

    To modify in place the elements of vector, a new kind of syntax (using auto&&) must be used:

    for (auto&& x : v)
        x = !x;
    

    The following code works fine:

    vector v = {true, false, false, true};
    
    // Invert boolean status
    for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
        x = !x;
    
    // Print new element values
    cout << boolalpha;        
    for (const auto& x : v)
        cout << x << ' ';
    

    and outputs:

    false true true false
    

    Note that the for (auto&& elem : container) syntax also works in the other cases of ordinary (non-proxy) iterators (e.g. for a vector or a vector).

    (As a side note, the aforementioned "observing" syntax of for (const auto& elem : container) works fine also for the proxy iterator case.)

    Summary

    The above discussion can be summarized in the following guidelines:

    1. For observing the elements, use the following syntax:

      for (const auto& elem : container)    // capture by const reference
      
      • If the objects are cheap to copy (like ints, doubles, etc.), it's possible to use a slightly simplified form:

        for (auto elem : container)    // capture by value
        
    2. For modifying the elements in place, use:

      for (auto& elem : container)    // capture by (non-const) reference
      
      • If the container uses "proxy iterators" (like std::vector), use:

        for (auto&& elem : container)    // capture by &&
        

    Of course, if there is a need to make a local copy of the element inside the loop body, capturing by value (for (auto elem : container)) is a good choice.


    Additional notes on generic code

    In generic code, since we can't make assumptions about generic type T being cheap to copy, in observing mode it's safe to always use for (const auto& elem : container).
    (This won't trigger potentially expensive useless copies, will work just fine also for cheap-to-copy types like int, and also for containers using proxy-iterators, like std::vector.)

    Moreover, in modifying mode, if we want generic code to work also in case of proxy-iterators, the best option is for (auto&& elem : container).
    (This will work just fine also for containers using ordinary non-proxy-iterators, like std::vector or std::vector.)

    So, in generic code, the following guidelines can be provided:

    1. For observing the elements, use:

      for (const auto& elem : container)
      
    2. For modifying the elements in place, use:

      for (auto&& elem : container)
      

提交回复
热议问题