Ủy ban tiêu chuẩn C ++ có dự định rằng trong C ++ 11 không có thứ tự_map sẽ phá hủy những gì nó chèn vào không?


114

Tôi vừa mất ba ngày trong cuộc đời mình để tìm ra một lỗi rất lạ trong đó unardered_map :: insert () phá hủy biến bạn chèn. Hành vi rất không rõ ràng này chỉ xảy ra trong các trình biên dịch gần đây: Tôi thấy rằng clang 3.2-3.4 và GCC 4.8 là những trình biên dịch duy nhất chứng minh "tính năng" này.

Dưới đây là một số mã giảm từ cơ sở mã chính của tôi, chứng tỏ sự cố:

#include <memory>
#include <unordered_map>
#include <iostream>

int main(void)
{
  std::unordered_map<int, std::shared_ptr<int>> map;
  auto a(std::make_pair(5, std::make_shared<int>(5)));
  std::cout << "a.second is " << a.second.get() << std::endl;
  map.insert(a); // Note we are NOT doing insert(std::move(a))
  std::cout << "a.second is now " << a.second.get() << std::endl;
  return 0;
}

Tôi, có lẽ giống như hầu hết các lập trình viên C ++, mong đợi đầu ra trông giống như sau:

a.second is 0x8c14048
a.second is now 0x8c14048

Nhưng với clang 3.2-3.4 và GCC 4.8, tôi nhận được điều này thay thế:

a.second is 0xe03088
a.second is now 0

Điều này có thể không có ý nghĩa gì, cho đến khi bạn kiểm tra kỹ lưỡng các tài liệu cho unardered_map :: insert () tại http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ trong đó quá tải số 2 là:

template <class P> pair<iterator,bool> insert ( P&& val );

Đó là một quá tải di chuyển tham chiếu phổ quát tham lam, sử dụng bất kỳ thứ gì không khớp với bất kỳ quá tải nào khác và chuyển cấu trúc nó thành một value_type. Vậy tại sao đoạn mã của chúng ta ở trên lại chọn quá tải này, chứ không phải là quá tải không có thứ tự_map :: value_type như hầu hết mọi người mong đợi?

Câu trả lời khiến bạn nhìn chằm chằm vào mặt: unsrdered_map :: value_type là một cặp < const int, std :: shared_ptr> và trình biên dịch sẽ nghĩ chính xác rằng một cặp < int , std :: shared_ptr> không thể chuyển đổi. Do đó, trình biên dịch chọn quá tải tham chiếu phổ quát di chuyển, và điều đó sẽ phá hủy bản gốc, mặc dù lập trình viên không sử dụng std :: move () là quy ước điển hình để chỉ ra rằng bạn ổn với một biến bị hủy. Do đó, hành vi hủy chèn trên thực tế là đúng theo tiêu chuẩn C ++ 11 và các trình biên dịch cũ hơn không chính xác .

Bây giờ bạn có thể thấy lý do tại sao tôi mất ba ngày để chẩn đoán lỗi này. Nó hoàn toàn không rõ ràng trong một cơ sở mã lớn nơi mà kiểu được chèn vào unardered_map là một typedef được định nghĩa ở xa trong các thuật ngữ mã nguồn và không bao giờ có người kiểm tra xem typedef có giống với value_type hay không.

Vì vậy, các câu hỏi của tôi đối với Stack Overflow:

  1. Tại sao các trình biên dịch cũ hơn không hủy các biến được chèn vào như các trình biên dịch mới hơn? Ý tôi là, ngay cả GCC 4.7 cũng không làm điều này, và nó tuân theo các tiêu chuẩn khá tốt.

  2. Vấn đề này có được biết đến rộng rãi không, bởi vì chắc chắn việc nâng cấp trình biên dịch sẽ khiến mã đã từng hoạt động đột ngột ngừng hoạt động?

  3. Ủy ban tiêu chuẩn C ++ có dự định hành vi này không?

  4. Làm thế nào bạn đề xuất rằng không có thứ tự_map :: insert () được sửa đổi để cung cấp hành vi tốt hơn? Tôi hỏi điều này vì nếu có sự hỗ trợ ở đây, tôi dự định gửi hành vi này như một ghi chú N cho WG21 và yêu cầu họ thực hiện một hành vi tốt hơn.


10
Chỉ vì nó sử dụng một tham chiếu phổ biến không có nghĩa là giá trị được chèn vào luôn được di chuyển - nó chỉ nên làm như vậy đối với các giá trị, mà đơn giản alà không. Nó sẽ tạo ra một bản sao. Ngoài ra, hành vi này hoàn toàn phụ thuộc vào stdlib, không phải trình biên dịch.
Xeo

10
Điều đó có vẻ như là một lỗi trong việc triển khai thư viện
David Rodríguez - dribeas

4
"Do đó, hành vi hủy chèn trên thực tế là đúng theo tiêu chuẩn C ++ 11 và các trình biên dịch cũ hơn là không chính xác." Xin lỗi, nhưng bạn đã nhầm. Bạn lấy ý tưởng đó từ phần nào của Chuẩn C ++? BTW cplusplus.com không phải là chính thức.
Ben Voigt

1
Tôi không thể tái tạo điều này trên hệ thống của mình và tôi đang sử dụng gcc 4.8.2 và 4.9.0 20131223 (experimental)tương ứng. Đầu ra là a.second is now 0x2074088 (hoặc tương tự) đối với tôi.

47
Đây là lỗi GCC 57619 , một hồi quy trong chuỗi 4.8 đã được sửa cho 4.8.2 trong 2013-06.
Casey

Câu trả lời:


83

Như những người khác đã chỉ ra trong các nhận xét, hàm tạo "phổ quát" trên thực tế không phải luôn luôn di chuyển khỏi đối số của nó. Nó phải di chuyển nếu đối số thực sự là một rvalue và sao chép nếu nó là một lvalue.

Bạn quan sát thấy hành vi luôn di chuyển, là một lỗi trong libstdc ++, hiện đã được sửa theo nhận xét về câu hỏi. Đối với những người tò mò, tôi đã xem xét các tiêu đề g ++ - 4.8.

bits/stl_map.h, dòng 598-603

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h, dòng 365-370

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

Sau đó là sử dụng không chính xác std::movenơi cần sử dụng std::forward.


11
Clang sử dụng libstdc ++ theo mặc định, stdlib của GCC.
Xeo

Tôi đang sử dụng gcc 4.9 và tôi đang tìm kiếm libstdc++-v3/include/bits/. Tôi không thấy điều tương tự. Tôi hiểu rồi { return _M_h.insert(std::forward<_Pair>(__x)); }. Nó có thể khác với 4.8, nhưng tôi chưa kiểm tra.

Vâng, vì vậy tôi đoán họ đã sửa lỗi.
Brian

@Brian Nope, tôi vừa kiểm tra tiêu đề hệ thống của mình. Điều tương tự. 4.8.2 btw.

Của tôi là 4.8.1, vì vậy tôi đoán nó đã được sửa giữa hai.
Brian

20
template <class P> pair<iterator,bool> insert ( P&& val );

Đó là một quá tải di chuyển tham chiếu phổ quát tham lam, sử dụng bất kỳ thứ gì không khớp với bất kỳ quá tải nào khác và chuyển cấu trúc nó thành một value_type.

Đó là những gì một số người gọi là tham chiếu phổ quát , nhưng thực sự là tham chiếu sụp đổ . Trong trường hợp của bạn, nơi mà các đối số là một giá trị trái của loại pair<int,shared_ptr<int>>nó sẽ không dẫn đến việc tranh luận là một tài liệu tham khảo rvalue và nó không nên di chuyển từ nó.

Vậy tại sao đoạn mã của chúng ta ở trên lại chọn quá tải này, chứ không phải là quá tải không có thứ tự_map :: value_type như hầu hết mọi người mong đợi?

Bởi vì bạn, cũng như nhiều người khác trước đây, đã hiểu sai về nội dung value_typetrong vùng chứa. Trong value_typesố *map(cho dù có thứ tự hay không có thứ tự) là pair<const K, T>, trong trường hợp của bạn là như vậy pair<const int, shared_ptr<int>>. Loại không khớp sẽ loại bỏ tình trạng quá tải mà bạn có thể mong đợi:

iterator       insert(const_iterator hint, const value_type& obj);

Tôi vẫn không hiểu tại sao bổ đề mới này tồn tại, "tham chiếu phổ quát", không thực sự có nghĩa là bất kỳ điều gì cụ thể, cũng như nó không mang một cái tên hay cho một thứ hoàn toàn không "phổ biến" trong thực tế. Tốt hơn nhiều là bạn nên nhớ một số quy tắc thu gọn cũ là một phần của hành vi siêu ngôn ngữ C ++ như nó đã có kể từ khi các mẫu được giới thiệu trong ngôn ngữ này, cộng với chữ ký mới từ tiêu chuẩn C ++ 11. Lợi ích của việc nói về các tài liệu tham khảo phổ quát này là gì? Đã có những cái tên và những thứ đủ kỳ dị, như thế std::movechẳng động tĩnh gì cả.
user2485710

2
@ user2485710 Đôi khi, sự thiếu hiểu biết là niềm hạnh phúc và việc có thuật ngữ ô tô là "tham chiếu phổ quát" trên tất cả các tinh chỉnh thu gọn tham chiếu và loại trừ kiểu mẫu, IMHO khá khó hiểu, cộng với yêu cầu sử dụng std::forwardđể làm cho tinh chỉnh đó hoạt động thực sự ... Scott Meyers đã thực hiện tốt công việc thiết lập các quy tắc khá đơn giản để chuyển tiếp (sử dụng các tham chiếu phổ quát).
Mark Garcia

1
Theo suy nghĩ của tôi, "tham chiếu phổ quát" là một mẫu để khai báo các tham số mẫu hàm có thể liên kết với cả giá trị và giá trị. "Tham chiếu thu gọn" là điều xảy ra khi thay thế các tham số mẫu (được suy ra hoặc chỉ định) thành các định nghĩa, trong các tình huống "tham chiếu chung" và các ngữ cảnh khác.
aschepler

2
@aschepler: tham chiếu phổ quát chỉ là một cái tên ưa thích cho một tập hợp con của tham chiếu đang thu gọn. Tôi đồng ý với sự thiếu hiểu biết là niềm hạnh phúc , và thực tế là việc có một cái tên lạ mắt sẽ làm cho việc nói về nó trở nên dễ dàng và hợp xu hướng hơn và điều đó có thể giúp tuyên truyền hành vi. Điều đó được nói rằng tôi không phải là một người hâm mộ lớn của cái tên này, vì nó đẩy các quy tắc thực tế đến một góc mà chúng không cần được biết đến ... nghĩa là, cho đến khi bạn gặp phải một vụ án ngoài những gì Scott Meyers đã mô tả.
David Rodríguez - dribeas

Bây giờ vô nghĩa về ngữ nghĩa, nhưng sự khác biệt mà tôi đang cố gắng đề xuất: Tham chiếu phổ quát xảy ra khi tôi thiết kế và khai báo một tham số hàm dưới dạng tham số suy luận-khuôn mẫu &&; thu gọn tham chiếu xảy ra khi trình biên dịch khởi tạo một mẫu. Tham chiếu bị thu gọn là lý do khiến các tham chiếu phổ quát hoạt động, nhưng bộ não của tôi không thích đặt hai thuật ngữ vào cùng một miền.
aschepler
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.