Sao chép cấu trúc với các thành viên chưa được khởi tạo


29

Có hợp lệ để sao chép một cấu trúc mà một số thành viên không được khởi tạo không?

Tôi nghi ngờ đó là hành vi không xác định, nhưng nếu vậy, nó khiến cho bất kỳ thành viên nào chưa được khởi tạo trong một cấu trúc (ngay cả khi những thành viên đó không bao giờ được sử dụng trực tiếp) khá nguy hiểm. Vì vậy, tôi tự hỏi nếu có một cái gì đó trong tiêu chuẩn cho phép nó.

Ví dụ, điều này có hợp lệ không?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}

Tôi nhớ lại đã thấy một câu hỏi tương tự trước đây nhưng không thể tìm thấy nó. Câu hỏi này có liên quan như là câu hỏi này .
1201 Chương trình Chương trình

Câu trả lời:


23

Có, nếu thành viên chưa được khởi tạo không phải là loại ký tự hẹp không dấu hoặc std::byte, thì sao chép một cấu trúc có chứa giá trị không xác định này với hàm tạo sao chép được xác định ngầm là hành vi không xác định về mặt kỹ thuật, vì nó là để sao chép một biến có giá trị không xác định cùng loại, bởi vì của [dcl.init] / 12 .

Điều này áp dụng ở đây, bởi vì hàm tạo sao chép được tạo ngầm định, ngoại trừ unions, được xác định để sao chép từng thành viên riêng lẻ như thể bằng cách khởi tạo trực tiếp, xem [class.copy.ctor] / 4 .

Đây cũng là chủ đề của vấn đề CWG hoạt động 2264 .

Tôi cho rằng trong thực tế, bạn sẽ không có bất kỳ vấn đề với điều đó, mặc dù.

Nếu bạn muốn chắc chắn 100%, việc sử dụng std::memcpyluôn có hành vi được xác định rõ nếu loại có thể sao chép tầm thường , ngay cả khi các thành viên có giá trị không xác định.


Các vấn đề này sang một bên, bạn nên luôn luôn khởi tạo các thành viên lớp của mình đúng cách với một giá trị được chỉ định khi xây dựng, giả sử bạn không yêu cầu lớp phải có một hàm tạo mặc định tầm thường . Bạn có thể dễ dàng sử dụng cú pháp khởi tạo thành viên mặc định để ví dụ: khởi tạo giá trị cho các thành viên:

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}

tốt .. cấu trúc đó không phải là POD (Dữ liệu cũ đơn giản)? Điều đó có nghĩa là các thành viên sẽ được khởi tạo với các giá trị mặc định? Đó là một nghi ngờ
Kevin Kouketsu

Đây không phải là bản sao nông trong trường hợp này sao? Điều gì có thể sai với điều này trừ khi thành viên chưa được truy cập trong cấu trúc được sao chép?
TruthSeeker

@KevinKouketsu Tôi đã thêm một điều kiện cho trường hợp yêu cầu loại POD tầm thường / POD.
quả óc chó

@TruthSeeker Tiêu chuẩn nói rằng đó là hành vi không xác định. Lý do nói chung là hành vi không xác định đối với các biến (không phải thành viên) được giải thích trong câu trả lời của AndreySemashev. Về cơ bản, nó là để hỗ trợ các biểu diễn bẫy với bộ nhớ chưa được khởi tạo. Cho dù điều này được dự định để áp dụng cho xây dựng bản sao tiềm ẩn của cấu trúc là câu hỏi về vấn đề CWG liên kết.
quả óc chó

@TruthSeeker Trình xây dựng sao chép ẩn được định nghĩa để sao chép từng thành viên riêng lẻ như thể bằng cách khởi tạo trực tiếp. Nó không được định nghĩa để sao chép biểu diễn đối tượng như thể bởi memcpy, ngay cả đối với các loại có thể sao chép tầm thường. Ngoại lệ duy nhất là các hiệp hội, mà hàm tạo sao chép ẩn sẽ sao chép biểu diễn đối tượng như thể bởi memcpy.
quả óc chó

11

Nói chung, sao chép dữ liệu chưa được khởi tạo là hành vi không xác định vì dữ liệu đó có thể ở trạng thái bẫy. Trích dẫn này trang:

Nếu một đại diện đối tượng không đại diện cho bất kỳ giá trị nào của loại đối tượng, nó được gọi là đại diện bẫy. Truy cập một đại diện bẫy theo bất kỳ cách nào khác ngoài việc đọc nó thông qua biểu thức giá trị của loại ký tự là hành vi không xác định.

NaN báo hiệu có thể cho các loại dấu phẩy động và trên một số số nguyên nền tảng có thể có biểu diễn bẫy.

Tuy nhiên, đối với các loại có thể sao chép tầm thường , có thể sử dụng memcpyđể sao chép biểu diễn thô của đối tượng. Làm như vậy là an toàn vì giá trị của đối tượng không được diễn giải và thay vào đó, chuỗi byte thô của biểu diễn đối tượng được sao chép.


Điều gì về dữ liệu của các loại mà tất cả các mẫu bit đại diện cho các giá trị hợp lệ (ví dụ: cấu trúc 64 byte chứa một unsigned char[64])? Việc coi các byte của một cấu trúc là có các giá trị Không xác định có thể cản trở tối ưu hóa một cách không cần thiết, nhưng yêu cầu các lập trình viên tự điền vào mảng với các giá trị vô dụng sẽ cản trở hiệu quả hơn nữa.
supercat

Khởi tạo dữ liệu không phải là vô ích, nó ngăn chặn UB, cho dù nguyên nhân là do biểu diễn bẫy hay do sử dụng dữ liệu chưa được khởi tạo sau này. Không có 64 byte (1 hoặc 2 dòng bộ đệm) không đắt như nó có vẻ. Và nếu bạn có cấu trúc lớn, nơi đắt tiền, bạn nên suy nghĩ kỹ trước khi sao chép chúng. Và tôi khá chắc chắn rằng bạn sẽ phải khởi tạo chúng bằng mọi cách.
Andrey Semashev

Các hoạt động mã máy không thể ảnh hưởng đến hành vi của chương trình là vô ích. Quan niệm rằng bất kỳ hành động nào được đặc trưng là UB theo Tiêu chuẩn phải được tránh bằng mọi giá, thay vì nói rằng [theo lời của Ủy ban Tiêu chuẩn C] UB "xác định các khu vực có thể mở rộng ngôn ngữ phù hợp", là tương đối gần đây. Mặc dù tôi chưa thấy Cơ sở lý luận được công bố cho Tiêu chuẩn C ++, nhưng nó rõ ràng khước từ quyền tài phán đối với những gì chương trình C ++ được "cho phép" bằng cách từ chối phân loại các chương trình là tuân thủ hoặc không tuân thủ, có nghĩa là nó sẽ cho phép các phần mở rộng tương tự.
supercat

-1

Trong một số trường hợp, như mô tả, C ++ Standard cho phép trình biên dịch xử lý các cấu trúc theo bất kỳ cách nào mà khách hàng của họ sẽ thấy hữu ích nhất, mà không yêu cầu hành vi đó có thể dự đoán được. Nói cách khác, các cấu trúc như vậy gọi "Hành vi không xác định". Tuy nhiên, điều đó không ngụ ý rằng các cấu trúc như vậy có nghĩa là "bị cấm" vì Tiêu chuẩn C ++ rõ ràng khước từ quyền tài phán đối với những chương trình được hình thành tốt được "cho phép". Mặc dù tôi không biết về bất kỳ tài liệu Cơ sở lý luận nào được công bố cho Tiêu chuẩn C ++, nhưng thực tế là nó mô tả Hành vi không xác định giống như C89 sẽ cho thấy ý nghĩa dự định là tương tự: "Hành vi không xác định cho phép người thực hiện không gặp phải một số lỗi chương trình nhất định. để chẩn đoán.

Có nhiều tình huống trong đó cách hiệu quả nhất để xử lý một cái gì đó sẽ liên quan đến việc viết các phần của cấu trúc mà mã hạ nguồn sẽ quan tâm, trong khi bỏ qua những thứ mà mã hạ nguồn sẽ không quan tâm. Yêu cầu các chương trình khởi tạo tất cả các thành viên của một cấu trúc, bao gồm cả những chương trình mà không có gì sẽ quan tâm, sẽ không cần thiết cản trở hiệu quả.

Hơn nữa, có một số tình huống có thể hiệu quả nhất khi có dữ liệu chưa được xử lý theo cách không xác định. Ví dụ: đã cho:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

nếu mã xuôi dòng không quan tâm đến các giá trị của bất kỳ yếu tố nào x.dathoặc y.datcó chỉ số không được liệt kê trong đó arr, mã có thể được tối ưu hóa thành:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Sự cải thiện hiệu quả này sẽ không thể thực hiện được nếu các lập trình viên được yêu cầu viết rõ ràng mọi yếu tố của temp.dat , kể cả những người hạ lưu sẽ không quan tâm, trước khi sao chép nó.

Mặt khác, có một số ứng dụng rất quan trọng để tránh khả năng rò rỉ dữ liệu. Trong các ứng dụng như vậy, có thể có một phiên bản mã được sử dụng để bẫy bất kỳ nỗ lực nào để sao chép bộ nhớ chưa được khởi tạo mà không cần quan tâm đến việc liệu mã hạ nguồn có nhìn vào nó hay không, có thể hữu ích để đảm bảo việc thực hiện lưu trữ có nội dung có thể bị rò rỉ sẽ bị xóa hoặc ghi đè lên dữ liệu không bảo mật.

Từ những gì tôi có thể nói, Tiêu chuẩn C ++ không cố gắng nói rằng bất kỳ hành vi nào trong số này là đủ hữu ích hơn các hành vi khác để biện minh cho việc bắt buộc nó. Trớ trêu thay, việc thiếu đặc tả này có thể nhằm tạo điều kiện tối ưu hóa, nhưng nếu các lập trình viên không thể khai thác bất kỳ loại đảm bảo hành vi yếu nào, mọi tối ưu hóa sẽ bị từ chối.


-2

Vì tất cả các thành viên thuộc Dataloại nguyên thủy, data2sẽ nhận được "bản sao từng bit" chính xác của tất cả các thành viên data. Vì vậy, giá trị của data2.bsẽ chính xác giống như giá trị của data.b. Tuy nhiên, giá trị chính xác của data.bkhông thể dự đoán được, bởi vì bạn chưa khởi tạo nó một cách rõ ràng. Nó sẽ phụ thuộc vào giá trị của các byte trong vùng nhớ được phân bổ cho data.


Bạn có thể hỗ trợ điều này với một tham chiếu đến tiêu chuẩn? Các liên kết được cung cấp bởi @walnut ngụ ý đây là hành vi không xác định. Có một ngoại lệ cho POD trong tiêu chuẩn không?
Tomek Czajka

Mặc dù đây không phải là liên kết đến tiêu chuẩn, vẫn: en.cppreference.com/w/cpp/language/... "đối tượng TriviallyCopyable thể được sao chép bằng cách sao chép biểu tượng của họ bằng tay, ví dụ như với std :: memmove Tất cả các loại dữ liệu tương thích với C. ngôn ngữ (các loại POD) có thể sao chép tầm thường. "
ivan.ukr

"Hành vi không xác định" duy nhất trong trường hợp này là chúng ta không thể dự đoán giá trị của biến thành viên chưa được khởi tạo. Nhưng mã sẽ biên dịch và chạy thành công.
ivan.ukr

1
Đoạn bạn trích dẫn nói về hành vi của memmove, nhưng nó không thực sự có liên quan ở đây bởi vì trong mã của tôi, tôi đang sử dụng hàm tạo sao chép, không phải memmove. Các câu trả lời khác ngụ ý rằng việc sử dụng hàm tạo sao chép dẫn đến hành vi không xác định. Tôi nghĩ bạn cũng hiểu nhầm thuật ngữ "hành vi không xác định". Điều đó có nghĩa là ngôn ngữ không cung cấp sự đảm bảo nào cả, ví dụ chương trình có thể bị sập hoặc hỏng dữ liệu ngẫu nhiên hoặc làm bất cứ điều gì. Nó không chỉ có nghĩa là một số giá trị là không thể đoán trước, đó sẽ là hành vi không xác định.
Tomek Czajka

@ ivan.ukr Tiêu chuẩn C ++ chỉ định rằng các hàm tạo sao chép / di chuyển ẩn hoạt động thành viên khôn ngoan như thể bằng cách khởi tạo trực tiếp, xem các liên kết trong câu trả lời của tôi. Do đó, cấu trúc sao chép không tạo ra " " bản sao từng bit " ". Bạn chỉ đúng với các loại kết hợp, mà hàm tạo sao chép ẩn được chỉ định để sao chép biểu diễn đối tượng như thể bằng hướng dẫn std::memcpy. Không ai trong số này ngăn chặn sử dụng std::memcpyhoặc std::memmove. Nó chỉ ngăn chặn việc sử dụng các hàm tạo sao chép ẩn.
quả óc chó
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.