Có gì khác nhau giữa việc sử dụng struct và cặp std ::?


26

Tôi là một lập trình viên C ++ với kinh nghiệm hạn chế.

Giả sử tôi muốn sử dụng một STL mapđể lưu trữ và thao tác một số dữ liệu, tôi muốn biết liệu có sự khác biệt có ý nghĩa (cũng về hiệu suất) giữa hai cách tiếp cận cấu trúc dữ liệu đó không:

Choice 1:
    map<int, pair<string, bool> >

Choice 2:
    struct Ente {
        string name;
        bool flag;
    }
    map<int, Ente>

Cụ thể, có bất kỳ chi phí sử dụng structthay vì đơn giản pair?


18
A std::pair một cấu trúc.
Caleth

3
@gnat: Các câu hỏi chung như thế hiếm khi là mục tiêu dupe phù hợp cho các câu hỏi cụ thể như câu hỏi này, đặc biệt nếu câu trả lời cụ thể không tồn tại trên mục tiêu dupe (không có khả năng trong trường hợp này).
Robert Harvey

18
@Caleth - std::pairlà một mẫu . std::pair<string, bool>là một cấu trúc.
Pete Becker

4
pairlà hoàn toàn không có ngữ nghĩa. Không ai đọc mã của bạn (bao gồm cả bạn trong tương lai) sẽ biết đó e.firstlà tên của một cái gì đó trừ khi bạn chỉ ra một cách rõ ràng. Tôi là một người tin tưởng vững chắc vào đó pairlà một bổ sung rất nghèo nàn và lười biếng std, và khi nó được hình thành, không ai nghĩ rằng "nhưng một ngày nào đó, mọi người sẽ sử dụng điều này cho mọi thứ là hai điều, và không ai biết mã của ai có nghĩa là gì ".
Jason C

2
@Snowman ơi, chắc chắn rồi. Tuy nhiên, đó là những điều quá tệ như các maptrình lặp không phải là ngoại lệ hợp lệ. ("đầu tiên" = khóa và "thứ hai" = giá trị ... thực sự , std? Thật sao?)
Jason C

Câu trả lời:


33

Lựa chọn 1 là ok đối với những thứ "chỉ sử dụng một lần". Về cơ bản std::pairvẫn là một cấu trúc. Như đã nêu trong nhận xét này, lựa chọn 1 sẽ dẫn đến mã thực sự xấu xí ở đâu đó xuống lỗ thỏ như thế thing.second->first.second->secondvà không ai thực sự muốn giải mã điều đó.

Lựa chọn 2 tốt hơn cho mọi thứ khác, bởi vì nó dễ đọc hơn ý nghĩa của những thứ trong bản đồ là gì. Nó cũng linh hoạt hơn nếu bạn muốn thay đổi dữ liệu (ví dụ khi Ente đột nhiên cần một cờ khác). Hiệu suất không phải là một vấn đề ở đây.


15

Hiệu suất :

Nó phụ thuộc.

Trong trường hợp cụ thể của bạn sẽ không có sự khác biệt về hiệu năng vì cả hai sẽ được đặt tương tự trong bộ nhớ.

Trong một trường hợp rất cụ thể (nếu bạn đang sử dụng một cấu trúc trống làm một trong các thành viên dữ liệu) thì std::pair<>có khả năng có thể sử dụng Tối ưu hóa cơ sở trống (EBO) và có kích thước thấp hơn tương đương với cấu trúc. Và kích thước thấp hơn thường có nghĩa là hiệu suất cao hơn:

struct Empty {};
struct Thing { std::string name; Empty e; };

int main() {
    std::cout << sizeof(std::string) << "\n";
    std::cout << sizeof(std::tuple<std::string, Empty>) << "\n";
    std::cout << sizeof(std::pair<std::string, Empty>) << "\n";
    std::cout << sizeof(Thing) << "\n";
}

Bản in: 32, 32, 40, 40 trên ideone .

Lưu ý: Tôi không biết về bất kỳ triển khai nào thực sự sử dụng thủ thuật EBO cho các cặp thông thường, tuy nhiên nó thường được sử dụng cho các bộ dữ liệu.


Dễ đọc :

Tuy nhiên, ngoài tối ưu hóa vi mô, một cấu trúc được đặt tên là tiện dụng hơn.

Ý tôi là, map[k].firstnó không tệ trong khi get<0>(map[k])hầu như không thể hiểu được. Tương phản với map[k].nameđiều đó ngay lập tức chỉ ra những gì chúng ta đang đọc từ.

Tất cả đều quan trọng hơn khi các loại có thể chuyển đổi lẫn nhau, vì việc hoán đổi chúng vô tình trở thành mối quan tâm thực sự.

Bạn cũng có thể muốn đọc về Kết cấu và Đánh máy danh nghĩa. Entelà một loại cụ thể chỉ có thể được vận hành bởi những thứ mong đợi Ente, bất kỳ thứ gì có thể hoạt động std::pair<std::string, bool>đều có thể hoạt động trên chúng ... ngay cả khi std::stringhoặc boolkhông chứa những gì họ mong đợi, vì std::pairkhông có ngữ nghĩa liên quan đến nó.


Bảo trì :

Về mặt bảo trì, pairlà tồi tệ nhất. Bạn không thể thêm một trường.

tuplehội chợ tốt hơn trong vấn đề đó, miễn là bạn nối thêm trường mới, tất cả các trường hiện có vẫn được truy cập bởi cùng một chỉ mục. Điều này không thể hiểu được như trước đây nhưng ít nhất bạn không cần phải cập nhật chúng.

structlà người chiến thắng rõ ràng. Bạn có thể thêm các trường bất cứ nơi nào bạn cảm thấy thích nó.


Cuối cùng:

  • pair là tồi tệ nhất của cả hai thế giới,
  • tuple có thể có một cạnh nhẹ trong một trường hợp rất cụ thể (loại trống),
  • sử dụngstruct .

Lưu ý: nếu bạn sử dụng getters, thì bạn có thể tự sử dụng thủ thuật cơ sở trống mà không cần khách hàng phải biết về nó như trong struct Thing: Empty { std::string name; }; đó là lý do tại sao Encapsulation là chủ đề tiếp theo bạn nên quan tâm.


3
Bạn không thể sử dụng EBO cho các cặp, nếu bạn đang theo Tiêu chuẩn. Các yếu tố của cặp được lưu trữ trong các thành viên firstsecond, không có chỗ cho Tối ưu hóa cơ sở trống để khởi động.
Revolver_Ocelot

2
@Revolver_Ocelot: Chà, bạn không thể viết C ++ pairsẽ sử dụng EBO, nhưng trình biên dịch có thể cung cấp tích hợp sẵn. Vì những người được cho là thành viên, tuy nhiên, có thể quan sát được (ví dụ kiểm tra địa chỉ của họ) trong trường hợp đó sẽ không tuân thủ.
Matthieu M.

1
C ++ 20 cho biết thêm [[no_unique_address]], cho phép tương đương EBO cho các thành viên.
gạch dưới

3

Cặp tỏa sáng nhất khi được sử dụng làm kiểu trả về của hàm cùng với phép gán bị hủy bằng cách sử dụng ràng buộc có cấu trúc của std :: tie và C ++ 17. Sử dụng std :: tie:

struct Ente {/*...*/};
std::map<int, Ente> map;
auto inserted_position = map.end();
auto was_inserted = false;
std::tie(inserted_position, was_inserted) = map.emplace(1, Ente{});
if (!was_inserted) {
    //handle insertion error
}

Sử dụng ràng buộc có cấu trúc của C ++ 17:

struct Ente {/*...*/};
std::map<int, Ente> map;
auto [inserted_position, was_inserted] = map.emplace(1, Ente{});
if (!was_inserted) {
    //handle insertion error
}

Một ví dụ tồi về việc sử dụng std :: cặp (hoặc tuple) sẽ giống như thế này:

using player_data = std::tuple<std::string, uint64_t, double>;
player_data player{};
/* ... */
auto health = std::get<2>(player);
/* ... */

bởi vì không rõ ràng khi gọi std :: get <2> (player_data) những gì được lưu trữ ở chỉ số vị trí 2. Ghi nhớ khả năng đọc và làm cho người đọc thấy rõ những gì mã đang làm là quan trọng . Hãy xem xét rằng điều này dễ đọc hơn nhiều:

struct player_data
{
    std::string name;
    uint64_t player_id;
    double current_health;
};
player_data player{};
/* ... */
auto health = player.current_health;
/* ... */

Nói chung, bạn nên nghĩ về std :: cặp và std :: tuple như là cách để trả về hơn 1 đối tượng từ một hàm. Quy tắc ngón tay cái mà tôi sử dụng (và cũng đã thấy nhiều người khác sử dụng) là các đối tượng được trả về trong một cặp std :: tuple hoặc std :: chỉ "liên quan" trong bối cảnh thực hiện cuộc gọi đến một hàm trả về chúng hoặc trong bối cảnh cấu trúc dữ liệu liên kết chúng lại với nhau (ví dụ: std :: map sử dụng cặp std :: cho loại lưu trữ của nó). Nếu mối quan hệ tồn tại ở nơi khác trong mã của bạn, bạn nên sử dụng một cấu trúc.

Các phần liên quan của Nguyên tắc cốt lõi:

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.