Có an toàn để đẩy_back một phần tử từ cùng một vectơ không?


126
vector<int> v;
v.push_back(1);
v.push_back(v[0]);

Nếu Push_back thứ hai gây ra sự phân bổ lại, tham chiếu đến số nguyên đầu tiên trong vectơ sẽ không còn hợp lệ. Vì vậy, điều này không an toàn?

vector<int> v;
v.push_back(1);
v.reserve(v.size() + 1);
v.push_back(v[0]);

Điều này làm cho nó an toàn?


4
Một lưu ý: Hiện tại có một cuộc thảo luận trong diễn đàn đề xuất tiêu chuẩn. Là một phần của nó, ai đó đã đưa ra một ví dụ thực hiệnpush_back . Một poster khác ghi nhận một lỗi trong đó , đó là nó không xử lý đúng trường hợp bạn mô tả. Không ai khác, theo như tôi có thể nói, lập luận rằng đây không phải là một lỗi. Không nói đó là bằng chứng kết luận, chỉ là một quan sát.
Benjamin Lindley

9
Tôi xin lỗi nhưng tôi không biết nên chấp nhận câu trả lời nào vì vẫn còn tranh cãi về câu trả lời đúng.
Neil Kirk

4
Tôi đã được yêu cầu bình luận về câu hỏi này bởi bình luận thứ 5 trong phần: stackoverflow.com/a/18647445/576911 . Tôi đang làm như vậy bằng cách nâng cao mọi câu trả lời hiện đang nói: có, việc đẩy lùi một phần tử từ cùng một vectơ là an toàn.
Howard Hinnant

2
@BenVoigt: <nhún> Nếu bạn không đồng ý với những gì tiêu chuẩn nói, hoặc thậm chí nếu bạn đồng ý với tiêu chuẩn, nhưng đừng nghĩ nó nói đủ rõ ràng, đây luôn là một lựa chọn cho bạn: cplusplus.github.io/LWG/ lwg-active.html # submit_su Tôi đã tự mình thực hiện tùy chọn này nhiều lần hơn tôi có thể nhớ. Đôi khi thành công, đôi khi không. Nếu bạn muốn tranh luận về những gì tiêu chuẩn nói, hoặc những gì nó nên nói, SO không phải là một diễn đàn hiệu quả. Cuộc trò chuyện của chúng tôi không có ý nghĩa quy phạm. Nhưng bạn có thể có cơ hội ở một tác động thông thường bằng cách theo liên kết ở trên.
Howard Hinnant

2
@ Polaris878 Nếu Push_back làm cho vectơ đạt đến dung lượng của nó, vectơ sẽ phân bổ một bộ đệm mới lớn hơn, sao chép dữ liệu cũ và sau đó xóa bộ đệm cũ. Sau đó, nó sẽ chèn phần tử mới. Vấn đề là, phần tử mới là một tham chiếu đến dữ liệu trong bộ đệm cũ vừa bị xóa. Trừ khi Push_back tạo một bản sao của giá trị trước khi xóa, nó sẽ là một tham chiếu xấu.
Neil Kirk

Câu trả lời:


31

Có vẻ như http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closes.html#526 đã giải quyết vấn đề này (hoặc một cái gì đó rất giống với nó) như là một khiếm khuyết tiềm năng trong tiêu chuẩn:

1) Các tham số được lấy bởi tham chiếu const có thể được thay đổi trong khi thực hiện chức năng

Ví dụ:

Cho std :: vector v:

v.insert (v.begin (), v [2]);

v [2] có thể được thay đổi bằng cách di chuyển các phần tử của vectơ

Nghị quyết đề xuất là đây không phải là một khiếm khuyết:

vector :: insert (iter, value) là bắt buộc để hoạt động vì tiêu chuẩn không cho phép nó không hoạt động.


Tôi tìm thấy quyền trong 17.6.4.9: "Nếu một đối số cho hàm có giá trị không hợp lệ (chẳng hạn như giá trị bên ngoài miền của hàm hoặc con trỏ không hợp lệ cho mục đích sử dụng của nó), hành vi không được xác định." Nếu việc tái phân bổ xảy ra, thì tất cả các trình lặp và tham chiếu đến các phần tử đều không hợp lệ, có nghĩa là tham chiếu tham số được truyền cho hàm cũng không hợp lệ.
Ben Voigt

4
Tôi nghĩ vấn đề là việc thực hiện có trách nhiệm thực hiện việc tái phân bổ. Nó phụ thuộc vào nó để đảm bảo hành vi được xác định nếu đầu vào được xác định ban đầu. Vì thông số kỹ thuật xác định rõ ràng rằng Push_back tạo một bản sao, nên việc triển khai phải, bằng chi phí thời gian thực hiện, có thể lưu trữ bộ đệm hoặc sao chép tất cả các giá trị trước khi phân bổ lại. Vì trong câu hỏi cụ thể này không còn tham chiếu bên ngoài, nên việc lặp và tham chiếu không hợp lệ là không quan trọng.
OlivierD

3
@NeilKirk Tôi nghĩ rằng đây nên là câu trả lời có thẩm quyền, nó cũng được đề cập bởi Stephan T. Lavavej trên Reddit bằng cách sử dụng các đối số tương tự.
TemplateRex

v.insert(v.begin(), v[2]);không thể kích hoạt phân bổ lại. Vậy làm thế nào để trả lời câu hỏi này?
ThomasMcLeod ngày

@ThomasMcLeod: vâng, nó rõ ràng có thể kích hoạt việc tái phân bổ. Bạn đang mở rộng kích thước của vectơ bằng cách chèn một phần tử mới.
Violet Hươu cao cổ

21

Vâng, nó an toàn và việc triển khai thư viện tiêu chuẩn nhảy qua các vòng để làm cho nó trở nên như vậy.

Tôi tin rằng những người triển khai theo dõi yêu cầu này trở lại vào 23.2 / 11 bằng cách nào đó, nhưng tôi không thể tìm ra cách nào và tôi cũng không thể tìm thấy thứ gì cụ thể hơn. Điều tốt nhất tôi có thể tìm thấy là bài viết này:

http://www.drdobbs.com/cpp/copying-container-elements-from-the-c-li/240155771

Kiểm tra các triển khai của libc ++ và libstdc ++ cho thấy chúng cũng an toàn.


9
Một số hỗ trợ thực sự sẽ giúp ở đây.
chris

3
Điều đó thật thú vị, tôi phải thừa nhận tôi chưa bao giờ xem xét trường hợp này nhưng thực sự nó có vẻ khá khó khăn để đạt được. Nó cũng giữ cho vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.

2
@MatthieuM. Không: Bảng 100 cho biết: "pre: i và j không phải là các vòng lặp thành một".
Sebastian Redl

2
Bây giờ tôi đang ủng hộ vì đây là hồi ức của tôi, nhưng cần có một tài liệu tham khảo.
bames53

3
Là 23.2 / 11 trong phiên bản bạn đang sử dụng "Trừ khi có quy định khác (rõ ràng hoặc bằng cách xác định hàm theo các chức năng khác), gọi hàm thành viên bộ chứa hoặc chuyển một vùng chứa làm đối số cho hàm thư viện sẽ không làm mất hiệu lực của trình lặp đến hoặc thay đổi giá trị của các đối tượng trong vùng chứa đó. " ? Nhưng vector.push_backDOES chỉ định khác. "Gây ra sự phân bổ lại nếu kích thước mới lớn hơn công suất cũ." và (at reserve) "Tái phân bổ làm mất hiệu lực tất cả các tham chiếu, con trỏ và các trình vòng lặp tham chiếu đến các phần tử trong chuỗi."
Ben Voigt

13

Các tiêu chuẩn đảm bảo ngay cả ví dụ đầu tiên của bạn được an toàn. Trích dẫn C ++ 11

[trình tự.reqmts]

3 Trong Bảng 100 và 101 ... Xbiểu thị một lớp chứa trình tự, abiểu thị một giá trị Xchứa các phần tử loại T, ... tbiểu thị một giá trị hoặc một giá trị const củaX::value_type

16 Bảng 101 ...

Biểu thức a.push_back(t) Kiểu trả về void ngữ nghĩa hoạt động Bổ sung một bản sao của t. Yêu cầu: T sẽ được CopyInsertableđưa vào X. Container basic_string , deque, list,vector

Vì vậy, mặc dù nó không chính xác tầm thường, nhưng việc triển khai phải đảm bảo nó sẽ không làm mất hiệu lực tham chiếu khi thực hiện push_back.


7
Tôi không thấy cách này đảm bảo an toàn.
jrok

4
@Angew: Nó hoàn toàn không hợp lệ t, câu hỏi duy nhất là trước hoặc sau khi tạo bản sao. Câu cuối cùng của bạn chắc chắn là sai.
Ben Voigt

4
@BenVoigt Kể từ khi tđáp ứng các điều kiện tiên quyết được liệt kê, hành vi được mô tả được đảm bảo. Việc triển khai không được phép làm mất hiệu lực điều kiện tiên quyết và sau đó sử dụng điều đó như một lý do để không hành xử như được chỉ định.
bames53

8
@BenVoigt Khách hàng không bắt buộc phải duy trì điều kiện tiên quyết trong suốt cuộc gọi; chỉ để đảm bảo rằng nó được đáp ứng khi bắt đầu cuộc gọi.
bames53

6
@BenVoigt Đó là một điểm tốt nhưng tôi tin rằng có chức năng xử lý for_eachđược yêu cầu không làm mất hiệu lực các trình vòng lặp. Tôi không thể đưa ra một tài liệu tham khảo cho for_each, nhưng tôi thấy trên một số thuật toán văn bản như "op và binary_op sẽ không làm mất hiệu lực các trình lặp hoặc các phần phụ".
bames53

7

Không rõ ràng rằng ví dụ đầu tiên là an toàn, bởi vì cách thực hiện đơn giản nhất push_backlà trước tiên phân bổ lại vectơ, nếu cần, sau đó sao chép tham chiếu.

Nhưng ít nhất nó có vẻ an toàn với Visual Studio 2010. Việc triển khai push_backxử lý trường hợp đặc biệt khi bạn đẩy lùi một phần tử trong vectơ. Mã được cấu trúc như sau:

void push_back(const _Ty& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
                    ...
        }
    else
        {   // push back a non-element
                    ...
        }
    }

8
Tôi muốn biết nếu đặc tả yêu cầu này là an toàn.
Nawaz

1
Theo Tiêu chuẩn, nó không bắt buộc phải an toàn. Tuy nhiên, có thể thực hiện nó một cách an toàn.
Ben Voigt

2
@BenVoigt Tôi muốn nói rằng nó được yêu cầu phải an toàn (xem câu trả lời của tôi).
Angew không còn tự hào về SO

2
@BenVoigt Tại thời điểm bạn vượt qua tham chiếu, nó là hợp lệ.
Angew không còn tự hào về SO

4
@Angew: Điều đó là không đủ. Bạn cần vượt qua một tham chiếu duy trì hiệu lực trong suốt thời gian của cuộc gọi và điều này thì không.
Ben Voigt

3

Đây không phải là một sự đảm bảo từ tiêu chuẩn, nhưng như một điểm dữ liệu khác, v.push_back(v[0])an toàn cho libc ++ của LLVM .

Cácstd::vector::push_back cuộc gọi của libc ++__push_back_slow_path khi cần phân bổ lại bộ nhớ:

void __push_back_slow_path(_Up& __x) {
  allocator_type& __a = this->__alloc();
  __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), 
                                                  size(), 
                                                  __a);
  // Note that we construct a copy of __x before deallocating
  // the existing storage or moving existing elements.
  __alloc_traits::construct(__a, 
                            _VSTD::__to_raw_pointer(__v.__end_), 
                            _VSTD::forward<_Up>(__x));
  __v.__end_++;
  // Moving existing elements happens here:
  __swap_out_circular_buffer(__v);
  // When __v goes out of scope, __x will be invalid.
}

Bản sao không chỉ phải được thực hiện trước khi giải phóng bộ nhớ hiện tại, mà trước khi chuyển từ các yếu tố hiện có. Tôi cho rằng việc di chuyển các yếu tố hiện có được thực hiện __swap_out_circular_buffer, trong trường hợp đó việc thực hiện này thực sự an toàn.
Ben Voigt

@BenVoigt: điểm tốt, và bạn thực sự đúng rằng việc di chuyển xảy ra bên trong __swap_out_circular_buffer. (Tôi đã thêm một số ý kiến ​​để lưu ý rằng.)
Nate Kohl

1

Phiên bản đầu tiên chắc chắn KHÔNG an toàn:

Các thao tác trên các trình vòng lặp thu được bằng cách gọi một bộ chứa thư viện tiêu chuẩn hoặc hàm thành viên chuỗi có thể truy cập vào bộ chứa bên dưới, nhưng không được sửa đổi nó. [Lưu ý: Cụ thể, các hoạt động của container làm mất hiệu lực các vòng lặp xung đột với các hoạt động trên các vòng lặp được liên kết với vùng chứa đó. - lưu ý cuối]

từ phần 17.6.5.9


Lưu ý rằng đây là phần nói về cuộc đua dữ liệu mà mọi người thường nghĩ đến kết hợp với luồng ... nhưng định nghĩa thực tế liên quan đến "xảy ra trước khi" mối quan hệ, và tôi không thấy bất kỳ mối quan hệ đặt hàng giữa nhiều tác dụng phụ của push_backtrong chơi ở đây, cụ thể là tính hợp lệ tham chiếu dường như không được định nghĩa là có trật tự đối với việc sao chép-xây dựng phần tử đuôi mới.


1
Cần hiểu rằng đó là một ghi chú, không phải là một quy tắc, vì vậy nó giải thích một hệ quả của quy tắc đã nói ở trên ... và hậu quả là giống hệt nhau đối với các tài liệu tham khảo.
Ben Voigt

5
Kết quả v[0]là không phải là một trình vòng lặp, tương tự như vậy, push_back()không có một trình vòng lặp. Vì vậy, từ góc độ luật sư ngôn ngữ, lập luận của bạn là vô hiệu. Lấy làm tiếc. Tôi biết, hầu hết các trình vòng lặp là con trỏ và điểm vô hiệu hóa trình lặp, khá giống với các tham chiếu, nhưng một phần của tiêu chuẩn mà bạn trích dẫn, không liên quan đến tình huống trong tay.
cmaster - phục hồi monica

-1. Đó là trích dẫn hoàn toàn không liên quan và dù sao cũng không trả lời. Ủy ban nói x.push_back(x[0])là AN TOÀN.
Nawaz

0

Nó hoàn toàn an toàn.

Trong ví dụ thứ hai của bạn, bạn có

v.reserve(v.size() + 1);

không cần thiết bởi vì nếu vectơ vượt quá kích thước của nó, nó sẽ ngụ ý reserve.

Vector chịu trách nhiệm cho công cụ này, không phải bạn.


-1

Cả hai đều an toàn vì Push_back sẽ sao chép giá trị, không phải tham chiếu. Nếu bạn đang lưu trữ các con trỏ, điều đó vẫn an toàn khi có liên quan đến vectơ, nhưng chỉ cần biết rằng bạn sẽ có hai yếu tố của vectơ của bạn trỏ đến cùng một dữ liệu.

Mục 23.2.1 Yêu cầu chung về container

16
  • a.push_back (t) Nối một bản sao của t. Yêu cầu: T phải là CopyInsertable thành X.
  • a.push_back (rv) Nối một bản sao của rv. Yêu cầu: T phải là MoveInsertable thành X.

Do đó, việc triển khai Push_back phải đảm bảo rằng một bản sao của v[0] được chèn vào. Bằng ví dụ ngược lại, giả sử một triển khai sẽ tái phân bổ trước khi sao chép, nó sẽ không chắc chắn nối thêm một bản sao v[0]và như vậy vi phạm các thông số kỹ thuật.


2
push_backtuy nhiên cũng sẽ thay đổi kích thước vectơ và trong một triển khai ngây thơ, điều này sẽ làm mất hiệu lực tham chiếu trước khi sao chép xảy ra. Vì vậy, trừ khi bạn có thể sao lưu điều này bằng một trích dẫn từ tiêu chuẩn, tôi sẽ xem xét nó sai.
Konrad Rudolph

4
Theo "này", bạn có nghĩa là ví dụ đầu tiên hoặc thứ hai? push_backsẽ sao chép giá trị vào vector; nhưng (theo như tôi có thể thấy) điều đó có thể xảy ra sau khi tái phân bổ, tại thời điểm đó, tham chiếu mà nó cố gắng sao chép không còn hợp lệ.
Mike Seymour

1
push_backnhận được đối số của nó bằng cách tham khảo .
bames53

1
@OlivierD: Nó sẽ phải (1) phân bổ không gian mới (2) sao chép phần tử mới (3) di chuyển - xây dựng các phần tử hiện có (4) phá hủy phần tử di chuyển (5) miễn phí bộ nhớ cũ - theo thứ tự THAT - để làm cho phiên bản đầu tiên hoạt động.
Ben Voigt

1
@BenVoigt tại sao một container khác lại yêu cầu một loại là CopyInsertable nếu nó sẽ hoàn toàn bỏ qua thuộc tính đó?
OlivierD

-2

Từ 23.3.6.5/1: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid.

Vì chúng tôi đang chèn vào cuối, nên sẽ không có tham chiếu nào bị vô hiệu nếu vectơ không được thay đổi kích thước. Vì vậy, nếu vectơ capacity() > size()thì nó được đảm bảo hoạt động, nếu không thì nó được đảm bảo là hành vi không xác định.


Tôi tin rằng các đặc điểm kỹ thuật thực sự đảm bảo điều này hoạt động trong cả hai trường hợp. Tôi đang chờ đợi trên một tài liệu tham khảo mặc dù.
bames53

Không có đề cập đến iterators hoặc iterator an toàn trong câu hỏi.
OlivierD

3
@OlivierD phần lặp lại là không cần thiết ở đây: Tôi quan tâm đến referencesphần trích dẫn.
Đánh dấu B

2
Nó thực sự được đảm bảo an toàn (xem câu trả lời của tôi, ngữ nghĩa của push_back).
Angew không còn tự hào về SO
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.