Ăn cắp tài nguyên từ khóa std :: map cho phép?


15

Trong C ++, có thể lấy cắp tài nguyên từ bản đồ mà tôi không cần sau đó nữa không? Chính xác hơn, giả sử tôi có một std::mapvới std::stringphím và tôi muốn xây dựng một véc tơ ra khỏi nó bằng cách ăn cắp các nguồn lực của mapcác phím s sử dụng std::move. Lưu ý rằng việc truy cập ghi vào các khóa làm hỏng cơ sở hạ tầng bên trong (thứ tự các khóa) của mapnhưng tôi sẽ không sử dụng nó sau đó.

Câu hỏi : Tôi có thể làm điều này mà không có bất kỳ vấn đề nào không hoặc điều này sẽ dẫn đến các lỗi không mong muốn, ví dụ như trong hàm hủy của mapvì tôi đã truy cập nó theo cách std::mapkhông dành cho?

Đây là một chương trình ví dụ:

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

Nó tạo ra đầu ra:

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

EDIT: Tôi muốn làm điều này cho toàn bộ nội dung của một bản đồ lớn và lưu các phân bổ động bằng cách đánh cắp tài nguyên này.


3
Mục đích của việc "ăn cắp" này là gì? Để loại bỏ các yếu tố khỏi bản đồ? Vậy thì tại sao không đơn giản làm điều đó (xóa phần tử khỏi bản đồ)? Ngoài ra, sửa đổi một constgiá trị luôn luôn là UB.
Một số lập trình viên anh chàng

rõ ràng nó sẽ dẫn đến lỗi nghiêm trọng!
rezaebrh

1
Không phải là câu trả lời trực tiếp cho câu hỏi của bạn, nhưng: Điều gì sẽ xảy ra nếu bạn không trả về một vectơ mà là một phạm vi hoặc một cặp vòng lặp? Điều đó sẽ tránh sao chép hoàn toàn. Trong mọi trường hợp, bạn cần điểm chuẩn để theo dõi tiến trình tối ưu hóa và trình lược tả để tìm các điểm nóng.
Ulrich Eckhardt

1
@ ALX23z Bạn có nguồn nào cho tuyên bố này không. Tôi không thể tưởng tượng làm thế nào sao chép một con trỏ đắt hơn so với sao chép toàn bộ vùng nhớ.
Sebastian Hoffmann

1
@SebastianHoffmann nó đã được đề cập trên CppCon gần đây không chắc chắn về cuộc nói chuyện nào, tho. Điều này std::stringcó tối ưu hóa chuỗi ngắn. Có nghĩa là có một số logic không tầm thường khi sao chép và di chuyển và không chỉ trao đổi con trỏ và ngoài ra hầu hết thời gian di chuyển ngụ ý sao chép - e rằng bạn xử lý các chuỗi khá dài. Sự khác biệt thống kê là dù sao nhỏ và nói chung, nó chắc chắn thay đổi tùy thuộc vào loại xử lý chuỗi được thực hiện.
ALX23z

Câu trả lời:


18

Bạn đang thực hiện hành vi không xác định, sử dụng const_castđể sửa đổi một constbiến. Đừng làm vậy. Lý do constlà vì bản đồ được sắp xếp theo các phím của chúng. Vì vậy, sửa đổi một khóa tại chỗ đang phá vỡ giả định cơ bản mà bản đồ được xây dựng.

Bạn không bao giờ nên sử dụng const_castđể loại bỏ constkhỏi một biến sửa đổi biến đó.

Điều đó được cho biết, C ++ 17 có giải pháp cho vấn đề của bạn: std::map's extractchức năng:

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

Và đừng using namespace std;. :)


Cảm ơn, tôi sẽ thử nó! Nhưng bạn có chắc chắn rằng tôi không thể làm như tôi đã làm? Tôi có nghĩa là mapsẽ không phàn nàn nếu tôi không gọi các phương thức của nó (điều mà tôi không làm) và có lẽ thứ tự nội bộ không quan trọng trong hàm hủy của nó?
phinz

2
Các phím của bản đồ được tạo const. Đột biến một constđối tượng là UB ngay lập tức, cho dù sau đó có bất kỳ thứ gì thực sự truy cập chúng hay không.
HTNW

Phương pháp này có hai vấn đề: (1) Vì tôi muốn trích xuất tất cả các yếu tố tôi không muốn trích xuất bằng khóa (tra cứu không hiệu quả) mà bằng iterator. Tôi đã thấy rằng điều này cũng có thể, vì vậy điều này là tốt. (2) Sửa lỗi cho tôi nếu tôi sai nhưng để trích xuất tất cả các yếu tố sẽ có một chi phí rất lớn (để cân bằng lại cấu trúc cây bên trong mỗi lần khai thác)?
phinz

2
@phinz Như bạn có thể thấy trên cppreference extract khi sử dụng các trình vòng lặp làm đối số có độ phức tạp không đổi. Một số chi phí là không thể tránh khỏi, nhưng nó có thể sẽ không đủ quan trọng để quan trọng. Nếu bạn có các yêu cầu đặc biệt không được bao gồm trong điều này, thì bạn sẽ cần phải thực hiện mapthỏa mãn các yêu cầu này của riêng bạn . Các stdthùng chứa có nghĩa là cho ứng dụng mục đích chung. Chúng không được tối ưu hóa cho các trường hợp sử dụng cụ thể.
quả óc chó

@HTNW Bạn có chắc chắn rằng các phím được tạo constkhông? Trong trường hợp này, bạn có thể vui lòng cho biết nơi tranh luận của tôi sai.
phinz

4

Mã của bạn cố gắng sửa đổi constcác đối tượng, vì vậy nó có hành vi không xác định, vì câu trả lời của druckermanly chỉ ra chính xác.

Một số câu trả lời khác ( phinz'sDeuchie ) cho rằng khóa không được lưu trữ dưới dạng constđối tượng vì xử lý nút dẫn đến việc trích xuất các nút ra khỏi bản đồ cho phép không consttruy cập vào khóa. Suy luận này ban đầu có vẻ hợp lý, nhưng P0083R3 , bài báo giới thiệu các extractchức năng), có một phần dành riêng cho chủ đề này làm mất hiệu lực đối số này:

Mối quan tâm

Một số lo ngại đã được nêu ra về thiết kế này. Chúng tôi sẽ giải quyết chúng ở đây.

Hành vi không xác định

Phần khó nhất của đề xuất này từ góc độ lý thuyết là thực tế là phần tử được trích xuất vẫn giữ kiểu khóa const của nó. Điều này ngăn chặn di chuyển ra khỏi nó hoặc thay đổi nó. Để giải quyết việc này, chúng tôi đã cung cấp chìa khóa chức năng accessor, cung cấp truy cập không const để chìa khóa trong các yếu tố tổ chức bởi tay cầm nút. Hàm này yêu cầu thực hiện "ma thuật" để đảm bảo rằng nó hoạt động chính xác với sự tối ưu hóa của trình biên dịch. Một cách để làm điều này là với một liên minh pair<const key_type, mapped_type>pair<key_type, mapped_type>. Việc chuyển đổi giữa chúng có thể được thực hiện một cách an toàn bằng cách sử dụng một kỹ thuật tương tự như được sử dụng bằng cách std::laundertrích xuất và lắp lại.

Chúng tôi không cảm thấy rằng điều này đặt ra bất kỳ vấn đề kỹ thuật hoặc triết học. Một trong những lý do bộ thư viện chuẩn tồn tại là để viết mã không cầm tay và huyền diệu mà khách hàng không thể viết bằng C ++ di động (ví dụ <atomic>, <typeinfo>, <type_traits>, vv). Đây chỉ là một ví dụ khác. Tất cả những gì các nhà cung cấp trình biên dịch yêu cầu để thực hiện phép thuật này là họ không khai thác các hành vi không xác định trong các hiệp hội cho mục đích tối ưu hóa và hiện tại các trình biên dịch đã hứa điều này (đến mức nó bị lợi dụng ở đây).

Điều này không áp đặt một hạn chế cho khách hàng rằng, nếu các chức năng này được sử dụng, std::pairkhông thể là chuyên ngành sao cho pair<const key_type, mapped_type>có bố cục khác với pair<key_type, mapped_type>. Chúng tôi cảm thấy khả năng bất cứ ai thực sự muốn làm điều này thực sự bằng không, và theo cách diễn đạt chính thức, chúng tôi hạn chế bất kỳ sự chuyên môn hóa nào của các cặp này.

Lưu ý rằng chức năng thành viên chính là nơi duy nhất cần các thủ thuật như vậy và không yêu cầu thay đổi đối với các thùng chứa hoặc cặp.

(nhấn mạnh của tôi)


Đây thực sự là một phần của câu trả lời cho câu hỏi ban đầu nhưng tôi chỉ có thể chấp nhận một.
phinz

0

Tôi không nghĩ rằng const_castvà sửa đổi dẫn đến hành vi không xác định trong trường hợp này nhưng xin vui lòng nhận xét liệu lập luận này có đúng không.

Câu trả lời này cho rằng

Nói cách khác, bạn nhận được UB nếu bạn sửa đổi một đối tượng const ban đầu, và nếu không thì không.

Vì vậy, dòng v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));trong câu hỏi không dẫn đến UB khi và chỉ khi stringđối tượng p.firstkhông được tạo dưới dạng đối tượng const. Bây giờ lưu ý rằng các tài liệu tham khảo vềextract các tiểu bang

Trích xuất một nút làm mất hiệu lực các trình vòng lặp cho phần tử được trích xuất. Con trỏ và tham chiếu đến phần tử được trích xuất vẫn hợp lệ, nhưng không thể được sử dụng trong khi phần tử được sở hữu bởi một nút điều khiển: chúng có thể sử dụng được nếu phần tử được chèn vào một thùng chứa.

Vì vậy, nếu tôi extractnode_handletương ứng với p, ptiếp tục sống ở vị trí lưu trữ của nó. Nhưng sau khi trích xuất, tôi được phép movebỏ đi các tài nguyên pnhư trong mã câu trả lời của druckermanly . Điều này có nghĩa là pvà do đó, stringđối tượng p.firstcũng không được tạo như đối tượng const ban đầu.

Do đó, tôi nghĩ rằng việc sửa đổi các mapkhóa không dẫn đến câu trả lời của UB và từ câu trả lời của Deuchie , dường như cấu trúc cây giờ đã bị hỏng (bây giờ là nhiều khóa chuỗi trống giống nhau) mapkhông gây ra sự cố trong hàm hủy. Vì vậy, mã trong câu hỏi nên hoạt động tốt ít nhất là trong C ++ 17 nơi extractphương thức tồn tại (và tuyên bố về con trỏ còn lại hợp lệ).

Cập nhật

Bây giờ tôi cho rằng câu trả lời này là sai. Tôi không xóa nó bởi vì nó được tham chiếu bởi các câu trả lời khác.


1
Xin lỗi, phinz, câu trả lời của bạn là sai. Tôi sẽ viết một câu trả lời để giải thích điều này - nó có liên quan đến các công đoàn và std::launder.
LF

-1

EDIT: Câu trả lời này là sai. Những bình luận tử tế đã chỉ ra những sai lầm, nhưng tôi không xóa nó vì nó đã được tham khảo trong các câu trả lời khác.

@druckermanly đã trả lời câu hỏi đầu tiên của bạn, trong đó nói rằng việc thay đổi các phím mapbằng lực sẽ phá vỡ sự ngăn nắp trong đó mapcấu trúc dữ liệu nội bộ được xây dựng (cây Đỏ-Đen). Nhưng nó an toàn khi sử dụng extractphương pháp vì nó thực hiện hai điều: di chuyển chìa khóa ra khỏi bản đồ và sau đó xóa nó, vì vậy nó không ảnh hưởng đến sự ngăn nắp của bản đồ.

Một câu hỏi khác mà bạn đặt ra, liệu nó có gây rắc rối khi giải cấu trúc không, không phải là vấn đề. Khi một bản đồ giải mã, nó sẽ gọi bộ giải mã của từng phần tử của nó (mapped_types, v.v.) và movephương thức đảm bảo rằng nó an toàn để giải mã một lớp sau khi nó được di chuyển. Vì vậy, đừng lo lắng. Tóm lại, đó là hoạt động của movenó đảm bảo an toàn để xóa hoặc gán lại một số giá trị mới cho lớp "đã di chuyển". Cụ thể string, movephương thức có thể đặt con trỏ char của nó thành nullptr, vì vậy nó không xóa dữ liệu thực tế đã được di chuyển khi bộ giải mã của lớp gốc được gọi.


Một bình luận nhắc nhở tôi về điểm mà tôi đang nhìn, về cơ bản anh ấy đã đúng nhưng có một điều tôi không hoàn toàn đồng ý: const_castcó lẽ không phải là một UB. constchỉ là một lời hứa giữa trình biên dịch và chúng tôi. các đối tượng được ghi chú constvẫn là một đối tượng, giống như các đối tượng không có const, về các kiểu và biểu diễn của chúng ở dạng nhị phân. Khi constbị bỏ đi, nó sẽ hành xử như thể nó là một lớp có thể thay đổi bình thường. Đối với move, nếu bạn muốn sử dụng nó, bạn phải vượt qua &thay vì a const &, vì vậy tôi thấy đó không phải là một UB, nó chỉ đơn giản là phá vỡ lời hứa constvà chuyển dữ liệu đi.

Tôi cũng đã thực hiện hai thí nghiệm, sử dụng MSVC 14.24.28314 và Clang 9.0.0 tương ứng và chúng cho kết quả tương tự.

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

đầu ra:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

Chúng ta có thể thấy rằng chuỗi '2' hiện đang trống, nhưng rõ ràng chúng ta đã phá vỡ sự ngăn nắp của bản đồ vì chuỗi trống phải được đặt lại ở phía trước. Nếu chúng ta cố gắng chèn, tìm hoặc xóa một số nút cụ thể của bản đồ, nó có thể gây ra thảm họa.

Dù sao, chúng tôi có thể đồng ý rằng không bao giờ nên thao túng dữ liệu bên trong của bất kỳ lớp nào bỏ qua giao diện công cộng của họ. Các find, insert, removechức năng, vv dựa đúng đắn của họ trên ngăn nắp của cấu trúc dữ liệu bên trong, và đó là lý do tại sao chúng ta nên tránh xa khi nghĩ đến nhìn trộm bên trong.


2
"liệu nó có gây rắc rối khi giải cấu trúc không, không phải là vấn đề." Về mặt kỹ thuật, vì hành vi không xác định (thay đổi constgiá trị) đã xảy ra trước đó. Tuy nhiên, đối số " movehàm [đảm bảo] đảm bảo an toàn khi giải cấu trúc [một đối tượng] một lớp sau khi nó được di chuyển" không giữ: Bạn không thể di chuyển an toàn từ một constđối tượng / tham chiếu, vì điều đó đòi hỏi phải sửa đổi constngăn chặn. Bạn có thể cố gắng khắc phục giới hạn đó bằng cách sử dụng const_cast, nhưng tại thời điểm đó, bạn tốt nhất sẽ thực hiện sâu sắc hành vi cụ thể, nếu không phải là UB.
hoffmale

@hoffmale Cảm ơn, tôi đã bỏ qua và mắc một lỗi lớn. Nếu đó không phải là bạn đã chỉ ra điều đó, câu trả lời của tôi ở đây có thể đánh lừa người khác. Trên thực tế tôi nên nói rằng movehàm này &thay vì a const&, vì vậy nếu người ta khăng khăng rằng anh ta di chuyển một chìa khóa ra khỏi bản đồ, anh ta phải sử dụng const_cast.
Deuchie

1
"Các đối tượng được ghi chú là const vẫn là một đối tượng, giống như các đối tượng không có const, về các kiểu và biểu diễn của chúng ở dạng nhị phân" Các đối tượng const có thể được đặt trong bộ nhớ chỉ đọc. Ngoài ra, const cho phép trình biên dịch suy luận về mã và lưu trữ giá trị thay vì tạo mã cho nhiều lần đọc (điều này có thể tạo ra sự khác biệt lớn về hiệu suất). Vì vậy, UB gây ra const_castsẽ trở nên đáng ghét. Nó có thể hoạt động hầu hết thời gian, nhưng phá vỡ mã của bạn theo những cách tinh tế.
LF

Nhưng ở đây tôi nghĩ rằng chúng ta có thể chắc chắn rằng các đối tượng không được đưa vào bộ nhớ chỉ đọc bởi vì sau đó extract, chúng ta được phép di chuyển từ cùng một đối tượng, phải không? (xem câu trả lời của tôi)
phinz

1
Xin lỗi, phân tích trong câu trả lời của bạn là sai. Tôi sẽ viết một câu trả lời để giải thích điều này - nó có cái gì để làm với các đoàn thể vàstd::launder
LF
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.