Cách chính xác để sử dụng phạm vi dựa trên phạm vi của C ++ 11 là gì?


211

Cách chính xác để sử dụng phạm vi dựa trên phạm vi của C ++ 11 là forgì?

Cú pháp nào nên được sử dụng? for (auto elem : container), hay for (auto& elem : container)hay for (const auto& elem : container)? Hay một số khác?


6
Xem xét tương tự áp dụng như cho các đối số chức năng.
Maxim Egorushkin

3
Trên thực tế, điều này ít liên quan đến phạm vi dựa trên. Điều tương tự có thể được nói về bất kỳ auto (const)(&) x = <expr>;.
Matthieu M.

2
@MatthieuM: Đây có rất nhiều để làm với phạm vi có trụ sở, tất nhiên! Hãy xem xét một người mới bắt đầu nhìn thấy một số cú pháp và không thể chọn hình thức để sử dụng. Quan điểm của "Q & A" là cố gắng làm sáng tỏ và giải thích sự khác biệt của một số trường hợp (và thảo luận về các trường hợp biên dịch tốt nhưng là loại không hiệu quả do bản sao sâu vô dụng, v.v.).
Mr.C64

2
@ Mr.C64: Theo như tôi biết, điều này có liên quan nhiều hơn auto, nói chung, hơn là dựa trên phạm vi cho; bạn hoàn toàn có thể sử dụng phạm vi dựa trên mà không cần bất kỳ auto! for (int i: v) {}là hoàn toàn tốt Tất nhiên, hầu hết các điểm bạn nêu trong câu trả lời của bạn có thể liên quan nhiều đến loại hơn so với auto... nhưng từ câu hỏi không rõ điểm đau ở đâu. Cá nhân, tôi sẽ tranh autocãi để loại bỏ khỏi câu hỏi; hoặc có thể làm cho nó rõ ràng rằng cho dù bạn sử dụng autohoặc đặt tên rõ ràng cho loại, câu hỏi được tập trung vào giá trị / tham chiếu.
Matthieu M.

1
@MatthieuM.: Tôi sẵn sàng thay đổi tiêu đề hoặc chỉnh sửa câu hỏi dưới một số hình thức có thể làm cho chúng rõ ràng hơn ... Một lần nữa, trọng tâm của tôi là thảo luận về một số tùy chọn cho các cú pháp dựa trên phạm vi (hiển thị mã biên dịch nhưng là không hiệu quả, mã không biên dịch, v.v.) và cố gắng đưa ra một số hướng dẫn cho ai đó (đặc biệt là ở cấp độ mới bắt đầu) tiếp cận dựa trên phạm vi C ++ 11 cho các vòng lặp.
Mr.C64

Câu trả lời:


389

Chúng ta hãy bắt đầu phân biệt giữa việc quan sát các yếu tố trong thùng chứa so với sửa đổi chúng tại chỗ.

Quan sát các yếu tố

Hãy xem xét một ví dụ đơn giản:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

Đoạn mã trên in các phần tử inttrong vector:

1 3 5 7 9

Bây giờ hãy xem xét một trường hợp khác, trong đó các phần tử vectơ không chỉ là các số nguyên đơn giản, mà là các thể hiện của một lớp phức tạp hơn, với hàm tạo sao chép tùy chỉnh, v.v.

// 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;
}

Nếu chúng ta sử dụng for (auto x : v) {...}cú pháp trên với lớp mới này:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

đầu ra giống như:

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Vì nó có thể được đọc từ đầu ra, copy constructor cuộc gọi được thực hiện trong phạm vi có trụ sở lặp loop.
Điều này là do chúng ta đang nắm bắt các phần tử từ container theo giá trị ( auto xphần trong for (auto x : v)).

Đây là mã không hiệu quả , ví dụ, nếu các yếu tố này là trường hợp std::string, phân bổ bộ nhớ heap có thể được thực hiện, với các chuyến đi đắt tiền đến trình quản lý bộ nhớ, v.v ... Điều này là vô ích nếu chúng ta chỉ muốn quan sát các yếu tố trong một container.

Vì vậy, một cú pháp tốt hơn có sẵn: chụp theo consttham chiếu , nghĩa là const auto&:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Bây giờ đầu ra là:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Không có bất kỳ cuộc gọi xây dựng sao chép giả (và có khả năng đắt tiền).

Vì vậy, khi quan sát các yếu tố trong một container (ví dụ, để truy cập read-only), cú pháp sau đây là tốt cho đơn giản giá rẻ-to-bản sao các loại, như int, double, v.v .:

for (auto elem : container) 

Khác, chụp bằng consttham chiếu là tốt hơn trong trường hợp chung , để tránh các cuộc gọi xây dựng sao chép vô dụng (và có khả năng tốn kém):

for (const auto& elem : container) 

Sửa đổi các thành phần trong container

Nếu chúng ta muốn sửa đổi các thành phần trong một thùng chứa bằng cách sử dụng phạm vi for, các cú pháp for (auto elem : container)for (const auto& elem : container)cú pháp ở trên là sai.

Trong thực tế, trong trường hợp trước, elemlưu trữ một bản sao của phần tử gốc, vì vậy các sửa đổi được thực hiện đối với phần tử đó chỉ bị mất và không được lưu trữ liên tục trong vùng chứa, ví dụ:

vector<int> 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 << ' ';

Đầu ra chỉ là chuỗi ban đầu:

1 3 5 7 9

Thay vào đó, một nỗ lực sử dụng for (const auto& x : v)chỉ thất bại để biên dịch.

g ++ xuất ra một thông báo lỗi như thế này:

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

Cách tiếp cận đúng trong trường hợp này là bắt bằng cách không consttham chiếu:

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

for (auto x : v)
    cout << x << ' ';

Đầu ra là (như mong đợi):

10 30 50 70 90

for (auto& elem : container)Cú pháp này cũng hoạt động đối với các loại phức tạp hơn, ví dụ: xem xét vector<string>:

vector<string> 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 << ' ';

đầu ra là:

Hi Bob! Hi Jeff! Hi Connie!

Trường hợp đặc biệt của trình vòng lặp proxy

Giả sử chúng ta có a vector<bool>và chúng ta muốn đảo ngược trạng thái boolean logic của các phần tử của nó, sử dụng cú pháp trên:

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

Các mã trên không thể biên dịch.

g ++ xuất ra một thông báo lỗi tương tự như sau:

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)
                    ^

Vấn đề là std::vectormẫu được chuyên cho bool, với một thực hiện mà gói các bools vào không gian tối ưu hóa (mỗi giá trị boolean được lưu trữ trong một chút, tám "boolean" bit trong một byte).

Do đó (vì không thể trả lại tham chiếu cho một bit), vector<bool>sử dụng mẫu được gọi là "trình lặp proxy" . "Trình lặp proxy" là một trình vòng lặp, khi được hủy đăng ký, không mang lại một thông thường bool &, mà thay vào đó trả về (theo giá trị) một đối tượng tạm thời , là một lớp proxy có thể chuyển đổi thànhbool . (Xem thêm câu hỏi này và các câu trả lời liên quan tại đây trên StackOverflow.)

Để sửa đổi các phần tử của vector<bool>, auto&&phải sử dụng một loại cú pháp mới (sử dụng ):

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

Các mã sau hoạt động tốt:

vector<bool> 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 << ' ';

và đầu ra:

false true true false

Lưu ý rằng for (auto&& elem : container)cú pháp cũng hoạt động trong các trường hợp khác của các trình vòng lặp thông thường (không phải proxy) (ví dụ: a vector<int>hoặc a vector<string>).

(Như một lưu ý phụ, cú pháp "quan sát" đã nói ở trên for (const auto& elem : container)cũng hoạt động tốt đối với trường hợp trình lặp proxy.)

Tóm lược

Các cuộc thảo luận ở trên có thể được tóm tắt trong các hướng dẫn sau:

  1. Để quan sát các yếu tố, sử dụng cú pháp sau:

    for (const auto& elem : container)    // capture by const reference
    • Nếu các đối tượng rẻ để sao chép (như ints, doubles, v.v.), có thể sử dụng một hình thức đơn giản hơn một chút:

      for (auto elem : container)    // capture by value
  2. Để sửa đổi các yếu tố tại chỗ, sử dụng:

    for (auto& elem : container)    // capture by (non-const) reference
    • Nếu vùng chứa sử dụng "trình lặp proxy" (như std::vector<bool>), hãy sử dụng:

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

Tất nhiên, nếu có nhu cầu tạo một bản sao cục bộ của phần tử bên trong thân vòng lặp, chụp theo giá trị ( for (auto elem : container)) là một lựa chọn tốt.


Ghi chú bổ sung về mã chung

Trong mã chung , vì chúng ta không thể đưa ra các giả định về loại chung Tlà rẻ để sao chép, trong chế độ quan sát, nó luôn an toàn để luôn sử dụng for (const auto& elem : container).
(Điều này sẽ không kích hoạt bản sao vô dụng khả năng tốn kém, sẽ chỉ làm việc tốt cũng với nhiều loại giá rẻ-to-bản sao như int, và cũng cho container sử dụng proxy-lặp, giống như std::vector<bool>.)

Hơn nữa, trong chế độ sửa đổi , nếu chúng ta muốn mã chung hoạt động trong trường hợp các trình lặp proxy, thì tùy chọn tốt nhất là for (auto&& elem : container).
(Điều này cũng sẽ hoạt động tốt đối với các container sử dụng các trình lặp không phải proxy thông thường, như std::vector<int>hoặc std::vector<string>.)

Vì vậy, trong mã chung , các hướng dẫn sau đây có thể được cung cấp:

  1. Để quan sát các yếu tố, sử dụng:

    for (const auto& elem : container)
  2. Để sửa đổi các yếu tố tại chỗ, sử dụng:

    for (auto&& elem : container)

7
Không có lời khuyên cho bối cảnh chung chung? :(
R. Martinho Fernandes

11
Tại sao không luôn luôn sử dụng auto&&? Có một const auto&&?
Martin Ba

1
Tôi đoán bạn đang thiếu trường hợp bạn thực sự cần một bản sao bên trong vòng lặp?
juanchopanza

6
"Nếu bộ chứa sử dụng" trình lặp proxy "" - bạn biết rằng nó sử dụng "trình lặp proxy" (có thể không phải là trường hợp trong mã chung). Vì vậy, tôi nghĩ rằng tốt nhất là thực sự auto&&, vì nó bao gồm auto&tốt như nhau.
Christian Rau

5
Cảm ơn bạn, đó là một "giới thiệu khóa học sự cố" thực sự tuyệt vời cho cú pháp và một số mẹo cho phạm vi dựa trên, dành cho lập trình viên C #. +1.
AndrewJacksonZA

17

Không có cách chính xác để sử dụng for (auto elem : container), for (auto& elem : container)hoặc for (const auto& elem : container). Bạn chỉ cần thể hiện những gì bạn muốn.

Hãy để tôi giải thích về điều đó. Chúng ta hãy đi dạo.

for (auto elem : container) ...

Đây là đường cú pháp cho:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Bạn có thể sử dụng cái này nếu thùng chứa của bạn chứa các yếu tố rẻ tiền để sao chép.

for (auto& elem : container) ...

Đây là đường cú pháp cho:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Ví dụ, sử dụng điều này khi bạn muốn ghi vào các phần tử trong vùng chứa.

for (const auto& elem : container) ...

Đây là đường cú pháp cho:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Như bình luận nói, chỉ để đọc. Và đó là về nó, mọi thứ đều "chính xác" khi được sử dụng đúng cách.


2
Tôi dự định đưa ra một số hướng dẫn, với việc biên dịch mã mẫu (nhưng không hiệu quả) hoặc không biên dịch, và giải thích lý do và cố gắng đề xuất một số giải pháp.
Mr.C64

2
@ Mr.C64 Ồ, tôi xin lỗi - Tôi vừa nhận thấy rằng đây là một trong những câu hỏi loại Câu hỏi thường gặp. Tôi mới đến trang web này. Xin lỗi! Câu trả lời của bạn rất hay, tôi đã nâng cấp nó - nhưng cũng muốn cung cấp một phiên bản ngắn gọn hơn cho những ai muốn ý chính của nó . Hy vọng, tôi không xâm nhập.

1
@ Mr.C64 có vấn đề gì với OP khi trả lời câu hỏi không? Nó chỉ là một câu trả lời khác, hợp lệ.
mfontanini

1
@mfontanini: Hoàn toàn không có vấn đề gì nếu ai đó đăng một số câu trả lời, thậm chí tốt hơn của tôi. Mục đích cuối cùng là đóng góp chất lượng cho cộng đồng (đặc biệt là cho những người mới bắt đầu có thể cảm thấy lạc lõng trước các cú pháp khác nhau và các tùy chọn khác nhau mà C ++ cung cấp).
Mr.C64

4

Phương tiện đúng luôn là

for(auto&& elem : container)

Điều này sẽ đảm bảo việc bảo tồn tất cả các ngữ nghĩa.


6
Nhưng điều gì sẽ xảy ra nếu container chỉ trả về các tham chiếu có thể sửa đổi và tôi muốn làm rõ rằng tôi không muốn sửa đổi chúng trong vòng lặp? Tôi không nên sử dụng auto const &để làm cho ý định của tôi rõ ràng?
RedX

@RedX: "Tham chiếu có thể sửa đổi" là gì?
Các cuộc đua nhẹ nhàng trong quỹ đạo

2
@RedX: Tài liệu tham khảo không bao giờ const, và chúng không bao giờ có thể thay đổi. Dù sao, câu trả lời của tôi cho bạn là có, tôi sẽ .
Các cuộc đua nhẹ nhàng trong quỹ đạo

4
Trong khi điều này có thể hoạt động, tôi cảm thấy đây là lời khuyên tồi so với cách tiếp cận nhiều sắc thái và được xem xét bởi câu trả lời xuất sắc và toàn diện của Mr.C64 được đưa ra ở trên. Việc giảm mẫu số ít phổ biến nhất không phải là những gì C ++ dành cho.
Jack Aidley

6
Đề xuất tiến hóa ngôn ngữ này đồng ý với câu trả lời "kém" này: open-std.org/jtc1/sc22/wg21/docs/ con / 2014 / n3853.htmlm
Luc Hermitte

1

Mặc dù động lực ban đầu của vòng lặp phạm vi có thể dễ dàng lặp lại qua các phần tử của một container, cú pháp đủ chung để hữu ích ngay cả đối với các đối tượng không hoàn toàn là container.

Yêu cầu cú pháp cho vòng lặp for là range_expressionhỗ trợ begin()end()là một trong hai hàm - với tư cách là các hàm thành viên của loại mà nó đánh giá hoặc là các hàm không phải thành viên, lấy một thể hiện của loại.

Như một ví dụ giả định, người ta có thể tạo ra một phạm vi số và lặp lại trên phạm vi bằng cách sử dụng lớp sau.

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

Với mainchức năng sau ,

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

người ta sẽ nhận được đầu ra sau đây.

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.